TQ57VE45BHV7MOZ6GKTYZEAMAOTXLPQ3ROCWJ2FUCITQWOYVMUIAC
export const IS_PRODUCTION = !window.location.href.includes("localhost") || window.location.href.includes("debug=false");
export const IS_DEVELOPMENT = !IS_PRODUCTION;
export const IS_DEBUG = (IS_DEVELOPMENT || window.location.href.includes("debug=true")) && !(window.location.href.includes("debug=false"));
const KeyInfo = () => ({
Q : false,
W : false,
E : false,
R : false,
T : false,
Y : false,
U : false,
I : false,
O : false,
P : false,
A : false,
S : false,
D : false,
F : false,
G : false,
H : false,
J : false,
K : false,
L : false,
Z : false,
X : false,
C : false,
V : false,
B : false,
N : false,
M : false,
Up : false,
Down : false,
Left : false,
Right : false,
Shift : false,
Spacebar: false,
Enter : false,
});
export type KeyInfoType = ReturnType<typeof KeyInfo>;
interface QueuedKeyboardEvent {
isDown: boolean;
event : KeyboardEvent;
}
export class KeyboardState {
public down = KeyInfo();
public justDown = KeyInfo();
public justUp = KeyInfo();
private _queuedEvents: QueuedKeyboardEvent[] = [];
constructor() {
document.addEventListener("keydown", e => this.keyDown(e), false);
document.addEventListener("keyup" , e => this.keyUp(e), false);
window.addEventListener("blur" , () => {
this.clear();
}, false);
}
public clear() {
this.down = KeyInfo();
this.justDown = KeyInfo();
this.justUp = KeyInfo();
this._queuedEvents = [];
}
private keyUp(e: KeyboardEvent): void {
// Since events usually happen between two ticks, we queue them up to be
// processed on the next tick.
this._queuedEvents.push({ event: e, isDown: false });
}
private keyDown(e: KeyboardEvent): void {
this._queuedEvents.push({ event: e, isDown: true });
}
private eventToKey(event: KeyboardEvent): string {
const number = event.keyCode || event.which;
let str: string;
switch (number) {
case 13: str = "Enter"; break;
case 16: str = "Shift"; break;
case 37: str = "Left" ; break;
case 38: str = "Up" ; break;
case 39: str = "Right"; break;
case 40: str = "Down" ; break;
/* A-Z */
default: str = String.fromCharCode(number);
}
if (str === " ") {
return "Spacebar";
}
if (str.length === 1) {
return str.toUpperCase();
}
return str[0].toUpperCase() + str.slice(1);
}
update(): void {
for (const key of Object.keys(this.justDown)) {
this.justDown[key as keyof KeyInfoType] = false;
this.justUp[key as keyof KeyInfoType] = false;
}
for (const queuedEvent of this._queuedEvents) {
const key = this.eventToKey(queuedEvent.event);
if (queuedEvent.isDown) {
if (!this.down[key as keyof KeyInfoType]) {
this.justDown[key as keyof KeyInfoType] = true;
}
this.down[key as keyof KeyInfoType] = true;
} else {
if (this.down[key as keyof KeyInfoType]) {
this.justUp[key as keyof KeyInfoType] = true;
}
this.down[key as keyof KeyInfoType] = false;
}
}
this._queuedEvents = [];
}
}
// 2D array that allows for negative indices
export class DefaultGrid<T> {
private _data: { [key: number]: { [key: number]: T} } = {};
private _makeDefault: (x: number, y: number) => T;
private _count = 0;
constructor(makeDefault: (x: number, y: number) => T) {
this._makeDefault = makeDefault;
}
getCount() {
return this._count;
}
keys(): { x: number, y: number }[] {
const result: { x: number, y: number }[] = [];
for (const x of Object.keys(this._data)) {
const inner = this._data[Number(x)];
for (const y of Object.keys(inner)) {
result.push({
x: Number(x),
y: Number(y),
});
}
}
return result;
}
values(): T[] {
const result: T[] = [];
for (const x of Object.keys(this._data)) {
const inner = this._data[Number(x)];
for (const y of Object.keys(inner)) {
result.push(inner[Number(y)]);
}
}
return result;
}
set(x: number, y: number, value: T) {
if (!this._data[x]) {
this._data[x] = {};
}
if (!this._data[x][y]) {
this._count++;
}
this._data[x][y] = value;
}
get(x: number, y: number): T {
if (!this._data[x]) {
this._data[x] = {};
}
if (this._data[x][y] === undefined) {
this._data[x][y] = this._makeDefault(x, y);
}
return this._data[x][y];
}
}
// 2D array that allows for negative indices
export class Grid<T> {
private _data: { [key: number]: { [key: number]: T} } = {};
getCount() {
let count = 0;
for (const key of Object.keys(this._data)) {
const inner = this._data[Number(key)];
count += Object.keys(inner).length;
}
return count;
}
keys(): { x: number, y: number }[] {
const result: { x: number, y: number }[] = [];
for (const x of Object.keys(this._data)) {
const inner = this._data[Number(x)];
for (const y of Object.keys(inner)) {
result.push({
x: Number(x),
y: Number(y),
});
}
}
return result;
}
set(x: number, y: number, value: T) {
if (!this._data[x]) {
this._data[x] = {};
}
this._data[x][y] = value;
}
get(x: number, y: number): T | null {
if (!this._data[x]) {
return null;
}
if (this._data[x][y] === undefined) {
return null;
}
return this._data[x][y];
}
getOrDefault(x: number, y: number, otherwise: T): T {
const result = this.get(x, y);
if (result === null) {
return otherwise;
} else {
return result;
}
}
}
export class HashSet<K extends { hash(): string }> {
private _values: HashMap<K, K>;
constructor(initialValues: K[] = []) {
this._values = new HashMap<K, K>();
for (const value of initialValues) {
this.put(value);
}
}
remove(key: K): void {
this._values.remove(key);
}
put(key: K): void {
this._values.put(key, key);
}
get(key: K): boolean {
return this._values.get(key) !== undefined;
}
values(): K[] {
return this._values.values();
}
}
export class HashMap<K extends { hash(): string }, V> {
private _values: { [key: string]: V } = {};
put(key: K, value: V) {
this._values[key.hash()] = value;
}
remove(key: K): void {
delete this._values[key.hash()];
}
get(key: K): V {
return this._values[key.hash()];
}
values(): V[] {
return Object.keys(this._values).map(key => this._values[key]);
}
}
export class DefaultHashMap<K extends { hash(): string }, V> {
private _values: { [key: string]: V } = {};
private _makeDefault: () => V;
constructor(makeDefaultValue: () => V) {
this._makeDefault = makeDefaultValue;
}
put(key: K, value: V) {
this._values[key.hash()] = value;
}
get(key: K): V {
if (this._values[key.hash()] === undefined) {
this._values[key.hash()] = this._makeDefault();
}
return this._values[key.hash()];
}
}
export class Pair<T extends { hash(): string }, U extends { hash(): string }> {
private _first: T;
private _second: U;
constructor(first: T, second: U) {
this._first = first;
this._second = second;
}
hash(): string {
return `${ this._first.hash() }|${ this._second.hash() }`
}
get first() {
return this._first;
}
get second() {
return this._second;
}
}
export const EPSILON = 0.0000001;
export const epsEqual = (x: number, y: number) => {
return Math.abs(x - y) < EPSILON;
}
export const epsGreaterThan = (x: number, y: number) => {
return (x + EPSILON - y) > 0;
}
export const epsLessThan = (x: number, y: number) => {
return (x - EPSILON - y) < 0;
}
import { Util } from '../util';
import { Line } from './line';
import { Rect } from './rect';
import { Vector2 } from './vector2';
const MAX_SIZE = 500;
export class ArbitrarySelection {
cover: Rect[] = [];
outlinesDirty = true;
oldOutlines: Line[][] = [];
constructor(cover: Rect[] = []) {
this.cover = cover;
}
public get x(): number {
if (this.cover.length === 0) { return 0; }
return Util.MinBy(this.cover, r => r.x)!.x;
}
public get y(): number {
if (this.cover.length === 0) { return 0; }
return Util.MinBy(this.cover, r => r.y)!.y;
}
public get w(): number {
if (this.cover.length === 0) { return 0; }
return Util.MaxBy(this.cover, r => r.right)!.right -
Util.MinBy(this.cover, r => r.x)!.x;
}
public get h(): number {
if (this.cover.length === 0) { return 0; }
return Util.MaxBy(this.cover, r => r.bottom)!.bottom -
Util.MinBy(this.cover, r => r.y)!.y;
}
public get pos(): Vector2 {
return new Vector2({ x: this.x, y: this.y });
}
public get bounds(): Rect {
return new Rect({
x: this.x,
y: this.y,
width: this.w,
height: this.h,
});
}
public get isEmpty(): boolean {
return this.cover.length === 0;
}
reset(): void {
this.cover = [];
this.oldOutlines = [];
}
addRect(rectToAdd: Rect): void {
const subsumingRects = this.cover.filter(r => r.completelyContains(rectToAdd));
const intersectingRects = this.cover.filter(r => r.intersects(rectToAdd, { edgesOnlyIsAnIntersection: false }));
if (subsumingRects.length > 0) {
return;
}
for (const rect of intersectingRects) {
this.subtractRect(rect.getIntersection(rectToAdd)!);
}
this.cover.push(rectToAdd);
this.outlinesDirty = true;
}
subtractRect(rectToSubtract: Rect): void {
const intersectingRects = this.cover.filter(r => r.intersects(rectToSubtract, { edgesOnlyIsAnIntersection: false }));
for (const rect of intersectingRects) {
// rectToSubtract completely contains rect
if (rectToSubtract.completelyContains(rect)) {
continue;
}
// rectToSubtract partially contains rect
const subrectToRemove = rectToSubtract.getIntersection(rect)!;
// rect completely contains subtractedRect
// -------------------------
// | A |
// | |
// |-----------------------|
// | B | hole | C |
// |-----------------------|
// | |
// | D |
// -------------------------
const newRects = [
{ x: rect.x , y: rect.y , width: rect.width , height: subrectToRemove.y - rect.y }, // A
{ x: rect.x , y: subrectToRemove.y , width: subrectToRemove.x - rect.x , height: subrectToRemove.height }, // B
{ x: subrectToRemove.x + subrectToRemove.width, y: subrectToRemove.y , width: rect.x + rect.width - (subrectToRemove.width + subrectToRemove.x), height: subrectToRemove.height }, // C
{ x: rect.x , y: subrectToRemove.y + subrectToRemove.height, width: rect.width , height: rect.y + rect.height - (subrectToRemove.y + subrectToRemove.height) }, // D
].filter(r => r.width > 0 && r.height > 0)
.map(r => new Rect(r));
this.cover = this.cover.concat(newRects);
}
for (const rect of intersectingRects) {
this.cover.splice(this.cover.indexOf(rect), 1);
}
this.outlinesDirty = true;
if (this.isEmpty) {
this.reset();
}
}
// O(n^2) scc algorithm until someone convinces me I need a faster one
getConnectedComponents(): Rect[][] {
const components: Rect[][] = [];
const seenRects: { [key: string]: boolean } = {}
for (const rect of this.cover) {
if (seenRects[rect.serialize()]) { continue; }
const component = this.getConnectedComponentFrom(rect);
components.push(component);
for (const seen of component) {
seenRects[seen.serialize()] = true;
}
}
return components;
}
private getConnectedComponentFrom(start: Rect): Rect[] {
const component: { [key: string]: boolean } = { };
let edge = [start];
while (edge.length > 0) {
let newEdge: Rect[] = [];
for (const rect of edge) {
if (component[rect.serialize()]) { continue; }
const intersectingRects = this.cover.filter(r => r.intersects(rect, { edgesOnlyIsAnIntersection: true }));
component[rect.serialize()] = true;
newEdge = newEdge.concat(intersectingRects);
}
edge = newEdge;
}
return Object.keys(component).map(r => Rect.DeserializeRect(r));
}
getOutlines(): Line[][] {
if (!this.outlinesDirty) {
return this.oldOutlines;
}
let result: Line[][] = [];
const components = this.getConnectedComponents();
for (const c of components) {
const outline = this.getOutlineFor(c);
const outlineComponents = this.getComponentsOfOutline(outline);
result = result.concat(outlineComponents)
}
this.oldOutlines = result;
this.outlinesDirty = false;
return result;
}
private getOutlineFor(comp: Rect[]): Line[] {
let allLines: (Line | undefined)[] = [];
for (const rect of comp) {
allLines.push.apply(allLines, rect.getLinesFromRect());
}
// Alternate solution if this proves too hard:
// Subdivide all lines on intersection points, then remove all
// duplicates.
// Actually that might even be better heh
// The strategy here is to basically remove all overlapping segments. it's
// hard because a single line could be overlapping with multiple other
// lines.
for (let i = 0; i < allLines.length; i++) {
const line1 = allLines[i];
if (!line1) { continue; }
for (let j = 0; j < allLines.length; j++) {
const line2 = allLines[j];
if (!line2) { continue; }
if (line1 === line2) { continue; }
const intersection = line1.getOverlap(line2);
if (intersection) {
allLines[i] = undefined;
allLines[j] = undefined;
const newLines = line1.getNonOverlappingSections(line2);
allLines = allLines.concat(newLines);
break;
}
}
}
return allLines.filter(l => l !== undefined) as Line[];
}
private getComponentsOfOutline(outline: Line[]): Line[][] {
// Store lookup table by start and end vertex
let lookupTable: { [key: number]: Line[] } = [];
for (const line of outline) {
const idx1 = line.x1 * MAX_SIZE + line.y1;
const idx2 = line.x2 * MAX_SIZE + line.y2;
if (!lookupTable[idx1]) { lookupTable[idx1] = []; }
if (!lookupTable[idx2]) { lookupTable[idx2] = []; }
lookupTable[idx1].push(line);
lookupTable[idx2].push(line);
}
let result: Line[][] = [];
let visited: { [key: string]: boolean } = {};
for (const line of outline) {
if (visited[line.serialized]) { continue; }
visited[line.serialized] = true;
const sequence = [line];
while (true) {
const current = sequence[sequence.length - 1];
const candidates = lookupTable[current.x1 * MAX_SIZE + current.y1].concat(lookupTable[current.x2 * MAX_SIZE + current.y2]);
const next = candidates.filter(l => l !== current && !visited[l.serialized])[0];
if (!next) { break; }
visited[next.serialized] = true;
sequence.push(next);
}
result.push(sequence);
}
return result;
}
addArbitraryShape(pixels: Vector2[], canvasSize: Vector2): void {
this.outlinesDirty = true;
const covered: boolean[] = new Array(MAX_SIZE * MAX_SIZE);
const rects: Rect[] = [];
const ll = pixels.length;
for (let i = 0; i < ll; i++) {
const p = pixels[i];
covered[p.x * MAX_SIZE + p.y] = false;
}
for (let x = 0; x < canvasSize.x; x++) {
for (let y = 0; y < canvasSize.y; y++) {
if (covered[x * MAX_SIZE + y] !== false) { continue; }
let squareSize = 2;
outer:
for (; squareSize < MAX_SIZE; squareSize++) {
const endSquareX = x + squareSize;
const endSquareY = y + squareSize;
for (let bottomLineX = x; bottomLineX < endSquareX; bottomLineX++) {
if (covered[bottomLineX * MAX_SIZE + (y + squareSize - 1)] === undefined ||
covered[bottomLineX * MAX_SIZE + (y + squareSize - 1)] === true) {
squareSize--;
break outer;
}
}
for (let bottomLineY = y; bottomLineY < endSquareY; bottomLineY++) {
if (covered[(x + squareSize - 1) * MAX_SIZE + bottomLineY] === undefined ||
covered[(x + squareSize - 1) * MAX_SIZE + bottomLineY] === true) {
squareSize--;
break outer;
}
}
}
for (let sx = x; sx < x + squareSize; sx++) {
for (let sy = y; sy < y + squareSize; sy++) {
covered[sx * MAX_SIZE + sy] = true;
}
}
rects.push(new Rect({
x: x,
y: y,
width: squareSize,
height: squareSize,
}));
}
}
for (const r of rects) {
this.addRect(r);
}
}
clone(): ArbitrarySelection {
const result = new ArbitrarySelection(this.cover.slice(0));
result.outlinesDirty = this.outlinesDirty;
result.oldOutlines = this.oldOutlines;
return result;
}
translate(p: Vector2): void {
this.cover = this.cover.map(x => x.translate(p));
this.oldOutlines = this.oldOutlines.map(l => l.map(ll => ll.translate(p)));
}
contains(p: Vector2): boolean {
if (this.cover.length === 0) { return true; }
for (const r of this.cover) {
if (r.contains(p)) { return true; }
}
return false;
}
}
import { Vector2 } from "./vector2"
import { Graphics } from "pixi.js";
import { epsGreaterThan, epsLessThan } from "../epsilon_math";
export class Line {
private _x1: number;
private _x2: number;
private _y1: number;
private _y2: number;
public get x1(): number { return this._x1; }
public get x2(): number { return this._x2; }
public get y1(): number { return this._y1; }
public get y2(): number { return this._y2; }
public get start(): Vector2 { return new Vector2({ x: this.x1, y: this.y1 }); }
public get end() : Vector2 { return new Vector2({ x: this.x2, y: this.y2 }); }
public get angleInDegrees(): number {
const cx = this._x1;
const cy = this._y1;
const ex = this._x2;
const ey = this._y2;
const dy = ey - cy;
const dx = ex - cx;
let theta = Math.atan2(dy, dx);
theta *= 180 / Math.PI;
if (theta < 0) {
theta = 360 + theta;
}
return theta;
}
public serialized = "";
constructor(props: { x1: number, x2: number, y1: number, y2: number } |
{ start: Vector2, end: Vector2 }) {
let x1, x2, y1, y2;
if ('x1' in props) {
x1 = props.x1;
x2 = props.x2;
y1 = props.y1;
y2 = props.y2;
} else {
x1 = props.start.x;
x2 = props.end.x;
y1 = props.start.y;
y2 = props.end.y;
}
this._x1 = x1;
this._y1 = y1;
this._x2 = x2;
this._y2 = y2;
this.serialized = `${ this.x1 }|${ this.x2 }|${ this.y1 }|${ this.y2 }`;
}
public get length(): number {
return Math.sqrt(
(this.x2 - this.x1) * (this.x2 - this.x1) +
(this.y2 - this.y1) * (this.y2 - this.y1)
);
}
public get isDegenerate(): boolean {
return this.length === 0;
}
public rotateAbout(origin: Vector2, angle: number): Line {
const start = this.start;
const end = this.end;
return new Line({
start: start.rotate(origin, angle),
end: end.rotate(origin, angle),
});
}
public scaleAbout(about: Vector2, amount: Vector2): Line {
return new Line({
start: this.start.scale(about, amount),
end: this.end.scale(about, amount),
});
}
sharesAVertexWith(other: Line): Vector2 | null {
if (this.start.equals(other.start)) { return this.start; }
if (this.start.equals(other.end)) { return this.start; }
if (this.end.equals(other.start)) { return this.end; }
if (this.end.equals(other.end)) { return this.end; }
return null;
}
static DeserializeLine(s: string): Line {
const [ x1, x2, y1, y2 ] = s.split("|").map(x => Number(x));
return new Line({ x1, x2, y1, y2 });
}
isXAligned(): boolean {
return this.x1 === this.x2;
}
isYAligned(): boolean {
return this.y1 === this.y2;
}
// Must be horizontally/vertically oriented lines
// Does not consider intersection, only overlap
getOverlap(other: Line): Line | undefined {
const orientedByX = (
this.x1 === this.x2 &&
this.x1 === other.x1 &&
this.x1 === other.x2
);
const orientedByY = (
this.y1 === this.y2 &&
this.y1 === other.y1 &&
this.y1 === other.y2
);
if (!orientedByX && !orientedByY) { return undefined; }
const summedLength = this.length + other.length;
const overallLength = new Line({
x1: Math.min(this.x1, other.x1),
y1: Math.min(this.y1, other.y1),
x2: Math.max(this.x2, other.x2),
y2: Math.max(this.y2, other.y2),
}).length;
if (overallLength >= summedLength) {
// These lines do not overlap.
return undefined;
}
if (orientedByX) {
return new Line({
x1: this.x1,
x2: this.x2,
y1: Math.max(this.y1, other.y1),
y2: Math.min(this.y2, other.y2),
});
} else /* if (orientedByY) */ {
return new Line({
y1: this.y1,
y2: this.y2,
x1: Math.max(this.x1, other.x1),
x2: Math.min(this.x2, other.x2),
});
}
}
// A----B----C----D
// AD - BC returns AB and CD.
getNonOverlappingSections(other: Line): Line[] | undefined {
const orientedByX = (
this.x1 === this.x2 &&
this.x1 === other.x1 &&
this.x1 === other.x2
);
const orientedByY = (
this.y1 === this.y2 &&
this.y1 === other.y1 &&
this.y1 === other.y2
);
if (!orientedByX && !orientedByY) { return undefined; }
const summedLength = new Line(this).length + new Line(other).length;
const overallLength = new Line({
x1: Math.min(this.x1, other.x1),
y1: Math.min(this.y1, other.y1),
x2: Math.max(this.x1, other.x1),
y2: Math.max(this.y1, other.y1),
}).length;
if (overallLength >= summedLength) {
// These lines do not overlap.
return undefined;
}
if (orientedByX) {
return [
new Line({ x1: this.x1, x2: this.x2, y1: Math.min(this.y1, other.y1), y2: Math.max(this.y1, other.y1), }),
new Line({ x1: this.x1, x2: this.x2, y1: Math.min(this.y2, other.y2), y2: Math.max(this.y2, other.y2), }),
].filter(l => !l.isDegenerate);
} else /* if (orientedByY) */ {
return [
new Line({ y1: this.y1, y2: this.y2, x1: Math.min(this.x1, other.x1), x2: Math.max(this.x1, other.x1), }),
new Line({ y1: this.y1, y2: this.y2, x1: Math.min(this.x2, other.x2), x2: Math.max(this.x2, other.x2), }),
].filter(l => !l.isDegenerate);
}
}
clone(): Line {
return new Line({ x1: this.x1, x2: this.x2, y1: this.y1, y2: this.y2 });
}
translate(p: Vector2): Line {
return new Line({
x1: this.x1 + p.x,
x2: this.x2 + p.x,
y1: this.y1 + p.y,
y2: this.y2 + p.y,
});
}
transform(trans: Vector2, scale: number): Line {
return new Line({
start: this.start.transform(trans, scale),
end: this.end.transform(trans, scale),
});
}
toJSON(): any {
return {
x1 : this.x1,
x2 : this.x2,
y1 : this.y1,
y2 : this.y2,
reviver: "Line",
};
}
toString(): string {
return `Line: [(${ this.x1 },${ this.y1 }) -> (${ this.x2 },${ this.y2 })]`;
}
equals(other: Line | null) {
if (other === null) { return false; }
return (
this.x1 === other.x1 &&
this.x2 === other.x2 &&
this.y1 === other.y1 &&
this.y2 === other.y2
) || (
this.x1 === other.x2 &&
this.x2 === other.x1 &&
this.y1 === other.y2 &&
this.y2 === other.y1
);
}
withNewEnd(newEnd: Vector2): Line {
return new Line({
x1: this.x1,
y1: this.y1,
x2: newEnd.x,
y2: newEnd.y,
});
}
withNewStart(newStart: Vector2): Line {
return new Line({
x1: newStart.x,
y1: newStart.y,
x2: this.x2,
y2: this.y2,
});
}
static Deserialize(obj: any): Line {
if (
!obj.hasOwnProperty("x1") ||
!obj.hasOwnProperty("y1") ||
!obj.hasOwnProperty("x2") ||
!obj.hasOwnProperty("y2")) {
console.error("Failed deserializing Rect");
}
return new Line({
x1: obj.x1,
y1: obj.y1,
x2: obj.x2,
y2: obj.y2,
});
}
static Serialize(obj: Line): string {
return JSON.stringify({
x1: obj.x1,
y1: obj.y1,
x2: obj.x2,
y2: obj.y2,
});
}
drawOnto(graphics: Graphics, color = 0xff0000) {
graphics.lineStyle(3, color, 1);
graphics.moveTo(this.x1, this.y1);
graphics.lineTo(this.x2, this.y2);
}
/**
* Returns the point where these two lines, if extended arbitrarily, would
* intersect.
*/
lineIntersection(other: Line): Vector2 {
const p1 = this.start;
const p2 = this.end;
const p3 = other.start;
const p4 = other.end;
const s = (
(p4.x - p3.x) *
(p1.y - p3.y) -
(p4.y - p3.y) *
(p1.x - p3.x)) / (
(p4.y - p3.y) *
(p2.x - p1.x) -
(p4.x - p3.x) *
(p2.y - p1.y)
);
const x = p1.x + s * (p2.x - p1.x);
const y = p1.y + s * (p2.y - p1.y);
return new Vector2({ x, y });
}
/**
* Returns the point where these two segments exist, if there is one.
*/
segmentIntersection(other: Line): Vector2 | null {
const lineIntersection = this.lineIntersection(other);
const x = lineIntersection.x;
const y = lineIntersection.y;
if (
(
// within us
epsGreaterThan(x, Math.min(this.x1, this.x2)) &&
epsLessThan (x, Math.max(this.x1, this.x2)) &&
epsGreaterThan(y, Math.min(this.y1, this.y2)) &&
epsLessThan (y, Math.max(this.y1, this.y2))
) && (
// within other
epsGreaterThan(x, Math.min(other.x1, other.x2)) &&
epsLessThan (x, Math.max(other.x1, other.x2)) &&
epsGreaterThan(y, Math.min(other.y1, other.y2)) &&
epsLessThan (y, Math.max(other.y1, other.y2))
)
) {
return lineIntersection;
}
return null;
}
normalize(): Line {
const mag = Math.sqrt(
(this.x1 - this.x2) ** 2 +
(this.y1 - this.y2) ** 2
);
return new Line({
start: this.start,
end: new Vector2({
x: this.start.x + (this.end.x - this.start.x) / mag,
y: this.start.x + (this.end.y - this.start.y) / mag,
})
})
}
hash(): string {
return this.toString();
}
add(x: Vector2): Line {
return new Line({
start: this.start.add(x),
end: this.end.add(x),
})
}
}
import { Line } from "./line";
import { Vector2, IVector2 } from "./vector2";
/**
* Immutable rectangle class.
*/
export class Rect {
private _x: number;
private _y: number;
private _width: number;
private _height: number;
public get x(): number {
return this._x;
}
public get y(): number {
return this._y;
}
public get width(): number {
return this._width;
}
public get height(): number {
return this._height;
}
public get centerX(): number {
return this._x + this._width / 2;
}
public get centerY(): number {
return this._y + this._height / 2;
}
public get right(): number {
return this._x + this._width;
}
public get bottom(): number {
return this._y + this._height;
}
public get top(): number {
return this._y;
}
public get left(): number {
return this._x;
}
public get center(): Vector2 {
return new Vector2({
x: this.x + this.width / 2,
y: this.y + this.height / 2,
});
}
public get dimensions(): Vector2 {
return new Vector2({ x: this.width, y: this.height });
}
public static FromPoint(point: IVector2, size: number): Rect {
return new Rect({
x: point.x,
y: point.y,
width: size,
height: size,
});
}
public static FromPoints(p1: IVector2, p2: IVector2): Rect {
return new Rect({
x: Math.min(p1.x, p2.x),
y: Math.min(p1.y, p2.y),
width: Math.abs(p1.x - p2.x),
height: Math.abs(p1.y - p2.y),
});
}
public static FromCenter(props: {
center: IVector2;
dimensions: IVector2;
}): Rect {
return new Rect({
x: props.center.x - props.dimensions.x / 2,
y: props.center.y - props.dimensions.y / 2,
width: props.dimensions.x,
height: props.dimensions.y,
});
}
public withRight(value: number): Rect {
return new Rect({
x: this.x,
y: this.y,
width: value - this.x,
height: this.height,
});
}
public withWidth(value: number): Rect {
return new Rect({
x: this.x,
y: this.y,
width: value,
height: this.height,
});
}
public withHeight(value: number): Rect {
return new Rect({
x: this.x,
y: this.y,
width: this.width,
height: value,
});
}
public withBottom(value: number): Rect {
return new Rect({
x: this.x,
y: this.y,
width: this.width,
height: value - this.y,
});
}
public withX(value: number): Rect {
return new Rect({
x: value,
y: this.y,
width: this.width,
height: this.height,
});
}
public withY(value: number): Rect {
return new Rect({
x: this.x,
y: value,
width: this.width,
height: this.height,
});
}
public withTop(value: number): Rect {
return this.withY(value);
}
public withLeft(value: number): Rect {
return this.withX(value);
}
/**
* bottomRight is held constant.
*/
public withTopLeft(topLeft: IVector2): Rect {
return Rect.FromPoints(topLeft, this.bottomRight);
}
/**
* bottomLeft is held constant.
*/
public withTopRight(topRight: IVector2): Rect {
return Rect.FromPoints(topRight, this.bottomLeft);
}
/**
* topLeft is held constant.
*/
public withBottomRight(bottomRight: IVector2): Rect {
return Rect.FromPoints(bottomRight, this.topLeft);
}
/**
* topRight is held constant.
*/
public withBottomLeft(bottomLeft: IVector2): Rect {
return Rect.FromPoints(bottomLeft, this.topRight);
}
public withCenter(center: IVector2): Rect {
return new Rect({
x: center.x - this.width / 2,
y: center.y - this.height / 2,
width: this.width,
height: this.height,
});
}
/**
* center is held constant
*/
public withScale(props: { width?: number; height?: number }): Rect {
return new Rect({
x: this.centerX - (props.width || this.width) / 2,
y: this.centerY - (props.height || this.height) / 2,
width: props.width || this.width,
height: props.height || this.height,
});
}
public get topLeft(): Vector2 {
return new Vector2({
x: this.x,
y: this.y,
});
}
public get topRight(): Vector2 {
return new Vector2({
x: this.right,
y: this.y,
});
}
public get bottomRight(): Vector2 {
return new Vector2({
x: this.right,
y: this.bottom,
});
}
public get bottomLeft(): Vector2 {
return new Vector2({
x: this.x,
y: this.bottom,
});
}
constructor(props: { x: number; y: number; width: number; height: number }) {
this._x = props.x;
this._y = props.y;
this._width = props.width;
this._height = props.height;
}
static DeserializeRect(s: string): Rect {
const [x, y, w, h] = s.split("|").map((x) => Number(x));
return new Rect({ x, y, width: w, height: h });
}
/**
* Return the four edges of this Rect as Lines.
*/
getLinesFromRect(): Line[] {
return [
new Line({ x1: this.x, y1: this.y, x2: this.x + this.width, y2: this.y }),
new Line({
x1: this.x,
y1: this.y,
x2: this.x,
y2: this.y + this.height,
}),
new Line({
x1: this.x + this.width,
y1: this.y + this.height,
x2: this.x + this.width,
y2: this.y,
}),
new Line({
x1: this.x + this.width,
y1: this.y + this.height,
x2: this.x,
y2: this.y + this.height,
}),
];
}
/**
* Return the four corners of this Rect.
*/
getCorners(): Vector2[] {
return [
new Vector2({ x: this.x, y: this.y }),
new Vector2({ x: this.x + this.width, y: this.y }),
new Vector2({ x: this.x, y: this.y + this.height }),
new Vector2({ x: this.x + this.width, y: this.y + this.height }),
];
}
serialize(): string {
return `${this.x}|${this.y}|${this.width}|${this.height}`;
}
// consider overlapping edges as intersection, but not overlapping corners.
intersects(
other: Rect,
props: { edgesOnlyIsAnIntersection: boolean } = {
edgesOnlyIsAnIntersection: false,
}
): boolean {
const intersection = this.getIntersection(other, true);
if (props.edgesOnlyIsAnIntersection) {
return (
!!intersection && (intersection.width > 0 || intersection.height > 0)
);
} else {
return !!intersection && intersection.width * intersection.height > 0;
}
}
completelyContains(smaller: Rect): boolean {
return (
this.x <= smaller.x &&
this.x + this.width >= smaller.x + smaller.width &&
this.y <= smaller.y &&
this.y + this.height >= smaller.y + smaller.height
);
}
getIntersection(
other: Rect,
edgesOnlyIsAnIntersection = false
): Rect | undefined {
const xmin = Math.max(this.x, other.x);
const xmax1 = this.x + this.width;
const xmax2 = other.x + other.width;
const xmax = Math.min(xmax1, xmax2);
if (xmax > xmin || (edgesOnlyIsAnIntersection && xmax >= xmin)) {
const ymin = Math.max(this.y, other.y);
const ymax1 = this.y + this.height;
const ymax2 = other.y + other.height;
const ymax = Math.min(ymax1, ymax2);
if (ymax >= ymin || (edgesOnlyIsAnIntersection && ymax >= ymin)) {
return new Rect({
x: xmin,
y: ymin,
width: xmax - xmin,
height: ymax - ymin,
});
}
}
return undefined;
}
contains(p: IVector2): boolean {
return (
p.x >= this.x &&
p.x < this.x + this.width &&
p.y >= this.y &&
p.y < this.y + this.height
);
}
clone(): Rect {
return new Rect({
x: this.x,
y: this.y,
width: this.width,
height: this.height,
});
}
add(p: IVector2): Rect {
return this.translate(p);
}
subtract(p: IVector2): Rect {
return this.translate({ x: -p.x, y: -p.y });
}
translate(p: IVector2): Rect {
return new Rect({
x: this.x + p.x,
y: this.y + p.y,
width: this.width,
height: this.height,
});
}
scale(p: Vector2): Rect {
return new Rect({
x: this.x,
y: this.y,
width: this.width * p.x,
height: this.height * p.y,
});
}
centeredAtOrigin(): Rect {
return new Rect({
x: -this.width / 2,
y: -this.height / 2,
width: this.width,
height: this.height,
});
}
equals(o: Rect | undefined | null): boolean {
if (!o) {
return false;
}
return (
this.x === o.x &&
this.y === o.y &&
this.width === o.width &&
this.height === o.height
);
}
toJSON(): any {
return {
x: this.x,
y: this.y,
w: this.width,
h: this.height,
reviver: "Rect",
};
}
hash(): string {
return this.toString();
}
/**
* Adds amount to both width and height.
*/
extend(amount: number): Rect {
return new Rect({
x: this.x,
y: this.y,
width: this.width + amount,
height: this.height + amount,
});
}
shrink(amount: number): Rect {
return new Rect({
x: this.x + amount,
y: this.y + amount,
width: Math.max(this.width - amount * 2, 0),
height: Math.max(this.height - amount * 2, 0),
});
}
floor(): Rect {
return new Rect({
x: Math.floor(this.x),
y: Math.floor(this.y),
width: Math.floor(this.width),
height: Math.floor(this.height),
});
}
/**
* Grow the Rect by amount in all directions.
*/
expand(amount: number): Rect {
return this.shrink(-amount);
}
transform(trans: Vector2, scale: number): Rect {
const topLeft = this.topLeft.transform(trans, scale);
const botRight = this.bottomRight.transform(trans, scale);
return new Rect({
x: topLeft.x,
y: topLeft.y,
width: botRight.x - topLeft.x,
height: botRight.y - topLeft.y,
});
}
static Deserialize(obj: any): Rect {
if (
!obj.hasOwnProperty("x") ||
!obj.hasOwnProperty("y") ||
!obj.hasOwnProperty("w") ||
!obj.hasOwnProperty("h")
) {
console.error("Failed deserializing Rect");
}
return new Rect({
x: obj.x,
y: obj.y,
width: obj.w,
height: obj.h,
});
}
static Serialize(r: Rect): string {
return JSON.stringify({
x: r.x,
y: r.y,
w: r.width,
h: r.height,
});
}
toString(): string {
return `[${this.x}, ${this.y}]`;
}
}
import { Rect } from "./rect";
import { Vector2 } from "./vector2";
export class RectGroup {
private _rects: Rect[];
constructor(rects: Rect[]) {
this._rects = rects;
}
intersects(other: Rect | RectGroup) {
if (other instanceof Rect) {
for (const rect of this._rects) {
if (rect.intersects(other)) {
return true;
}
}
return false;
}
if (other instanceof RectGroup) {
for (const r1 of this._rects) {
for (const r2 of this._rects) {
if (r1.intersects(r2)) {
return true;
}
}
}
return false;
}
}
getRects(): Rect[] {
return this._rects;
}
add(delta: Vector2): RectGroup {
const newRects = this._rects.map(rect => rect.add(delta));
return new RectGroup(newRects);
}
subtract(delta: Vector2): RectGroup {
const newRects = this._rects.map(rect => rect.subtract(delta));
return new RectGroup(newRects);
}
}
import { EPSILON } from "../epsilon_math";
import { Util } from "../util";
export interface IVector2 {
x: number;
y: number;
}
export class Vector2 {
private _x: number;
private _y: number;
public get x(): number { return this._x; }
public get y(): number { return this._y; }
constructor();
constructor(x: number, y: number);
constructor(props: { x: number, y: number });
constructor(propsOrX: { x: number, y: number } | number = { x: 0, y: 0 }, y?: number) {
if (typeof propsOrX === "number") {
this._x = propsOrX;
this._y = y!;
} else {
this._x = propsOrX.x;
this._y = propsOrX.y;
}
}
public get half(): Vector2 {
return new Vector2({ x: this.x / 2, y: this.y / 2 });
}
public static Zero: Vector2 = new Vector2(0, 0);
public static One: Vector2 = new Vector2(1, 1);
static IsVector2(x: any): x is Vector2 {
return x instanceof Vector2;
}
static Random(highX: number, highY: number, lowX = 0, lowY = 0) {
return new Vector2({
x: Util.RandRange(lowX, highX),
y: Util.RandRange(lowY, highY),
});
}
hash(): string {
return this.toString();
}
toString(): string {
return `[${this.x}, ${this.y}]`;
}
invert(): Vector2 {
return new Vector2({
x: -this.x,
y: -this.y,
});
}
round(): Vector2 {
return new Vector2({
x: Math.round(this.x),
y: Math.round(this.y),
});
}
floor(): Vector2 {
return new Vector2({
x: Math.floor(this.x),
y: Math.floor(this.y),
});
}
taxicabDistance(p: Vector2): number {
return Math.abs(p.x - this.x) + Math.abs(p.y - this.y);
}
diagonalDistance(p: IVector2): number {
return Math.max(Math.abs(p.x - this.x), Math.abs(p.y - this.y));
}
distance(p: IVector2): number {
let dx = Math.abs(p.x - this.x);
let dy = Math.abs(p.y - this.y);
return Math.sqrt(dx * dx + dy * dy);
}
translate(p: { x: number, y: number }): Vector2 {
return new Vector2({
x: this.x + p.x,
y: this.y + p.y,
});
}
subtract(p: { x: number, y: number }): Vector2 {
return new Vector2({
x: this.x - p.x,
y: this.y - p.y,
});
}
add(p: { x: number, y: number }): Vector2 {
return new Vector2({
x: this.x + p.x,
y: this.y + p.y,
});
}
addX(x: number): Vector2 {
return new Vector2({
x: this.x + x,
y: this.y,
});
}
addY(y: number): Vector2 {
return new Vector2({
x: this.x,
y: this.y + y,
});
}
subtractX(x: number): Vector2 {
return new Vector2({
x: this.x - x,
y: this.y,
});
}
subtractY(y: number): Vector2 {
return new Vector2({
x: this.x,
y: this.y - y,
});
}
clampY(low: number, high: number): Vector2 {
let newY = this.y;
if (newY < low) { newY = low; }
if (newY > high) { newY = high; }
return new Vector2({
x: this.x,
y: newY,
});
}
scale(about: { x: number; y: number }, amount: { x: number; y: number }): Vector2 {
return new Vector2({
x: (this.x - about.x) * amount.x + about.x,
y: (this.y - about.y) * amount.y + about.y,
});
}
rotate(origin: Vector2, angle: number): Vector2 {
angle = angle / (180 / Math.PI);
return new Vector2({
x: Math.cos(angle) * (this.x - origin.x) - Math.sin(angle) * (this.y - origin.y) + origin.x,
y: Math.sin(angle) * (this.x - origin.x) + Math.cos(angle) * (this.y - origin.y) + origin.y,
});
}
equals(other: Vector2 | undefined): boolean {
if (other === undefined) {
return false;
}
return (
Math.abs(this.x - other.x) < EPSILON &&
Math.abs(this.y - other.y) < EPSILON
);
}
multiply(other: Vector2 | number): Vector2 {
if (typeof other === "number") {
return new Vector2({
x: this.x * other,
y: this.y * other,
});
} else {
return new Vector2({
x: this.x * other.x,
y: this.y * other.y,
});
}
}
divide(other: Vector2 | number): Vector2 {
if (typeof other === "number") {
return new Vector2({
x: this.x / other,
y: this.y / other,
});
} else {
return new Vector2({
x: this.x / other.x,
y: this.y / other.y,
});
}
}
toJSON(): any {
return {
__type: "Vector2",
x: this.x,
y: this.y,
}
}
transform(trans: Vector2, scale: number): Vector2 {
return new Vector2({
x: Math.floor((this.x - trans.x) * scale),
y: Math.floor((this.y - trans.y) * scale),
});
}
normalize(): Vector2 {
if (this.x === 0 && this.y === 0) {
return this;
}
const length = Math.sqrt(this.x * this.x + this.y * this.y);
return new Vector2({
x: this.x / length,
y: this.y / length
});
}
withX(newX: number): Vector2 {
return new Vector2({
x: newX,
y: this.y,
});
}
withY(newY: number): Vector2 {
return new Vector2({
x: this.x,
y: newY,
});
}
invertX(): Vector2 {
return new Vector2({
x: -this.x,
y: this.y,
});
}
lerp(other: Vector2, t: number): Vector2 {
if (t > 1 || t < 0) { console.error("Lerp t must be between 0 and 1."); }
if (t === 0) return this;
if (t === 1) return other;
return this.scale({ x: 0, y: 0 }, { x: 1 - t, y: 1 - t }).add(other.scale({ x: 0, y: 0 }, { x: t, y: t }))
}
lerp2D(other: Vector2, tx: number, ty: number): Vector2 {
if (tx > 1 || tx < 0 || ty > 1 || ty < 0) { console.error("Lerp t must be between 0 and 1."); }
return this.scale({ x: 0, y: 0 }, { x: 1 - tx, y: 1 - ty }).add(other.scale({ x: 0, y: 0 }, { x: tx, y: ty }))
}
coserp(other: Vector2, t: number): Vector2 {
t = 0.5 * (1 + Math.cos(2 * t * Math.PI));
return this.lerp(other, t);
}
static Deserialize(obj: any): Vector2 {
if (!obj.hasOwnProperty("x") || !obj.hasOwnProperty("y")) {
console.error("Failed deserializing point");
}
return new Vector2({
x: obj.x,
y: obj.y,
});
}
static Serialize(obj: Vector2): string {
return JSON.stringify({ x: obj.x, y: obj.y });
}
}
let lastUsedId = 0;
export const getUniqueID = () => {
return lastUsedId++;
};
export class Util {
static MinBy<T>(list: T[], fn: (T: T) => number): T | null {
let lowestT: T | null = null;
let lowestValue: number | null = null;
for (const item of list) {
const value = fn(item);
if (lowestValue === null || value < lowestValue) {
lowestT = item;
lowestValue = value;
}
}
return lowestT;
}
static MinByAndValue<T>(list: T[], fn: (T: T) => number): { obj: T, value: number } | null {
let lowestT: T | null = null;
let lowestValue: number | null = null;
for (const item of list) {
const value = fn(item);
if (lowestValue === null || value < lowestValue) {
lowestT = item;
lowestValue = value;
}
}
return lowestT === null || lowestValue === null ? null : { obj: lowestT, value: lowestValue };
}
static MaxBy<T>(list: T[], fn: (T: T) => number): T | null {
let highestT: T | null = null;
let highestValue: number | null = null;
for (const item of list) {
const value = fn(item);
if (highestValue === null || value > highestValue) {
highestT = item;
highestValue = value;
}
}
return highestT;
}
static RandRange(low: number, high: number): number {
return Math.floor(Math.random() * (high - low) + low);
}
public static SortByKey<T>(array: T[], key: (x: T) => number): T[] {
return array.sort((a, b) => {
return key(a) - key(b)
});
}
public static ReplaceAll(
str: string,
mapObj: { [key: string]: string }
): string {
const re = new RegExp(Object.keys(mapObj).join('|'), 'gi')
return str.replace(re, matched => {
return mapObj[matched.toLowerCase()]
});
}
public static Debounce<F extends (...args: any[]) => void>(
func: F,
waitMilliseconds = 50,
options = {
isImmediate: false,
}
): F {
let timeoutId: any; // types are different on node vs client, so we have to use any.
const result = (...args: any[]) => {
const doLater = () => {
timeoutId = undefined;
if (!options.isImmediate) {
func.apply(this, args);
}
}
const shouldCallNow = options.isImmediate && timeoutId === undefined;
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(doLater, waitMilliseconds);
if (shouldCallNow) {
func.apply(this, args);
}
}
return result as any;
}
public static FormatDate(d: Date): string {
const monthName = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
][d.getMonth()]
return `${monthName} ${d.getDate()}, ${('00' + d.getHours()).substr(-2)}:${(
'00' + d.getMinutes()
).substr(-2)}:${('00' + d.getSeconds()).substr(-2)}`;
}
public static FlattenByOne<T>(arr: T[][]): T[] {
let result: T[] = []
for (const obj of arr) {
result = result.concat(obj)
}
return result
}
public static PadString(string: string, length: number, intersperse = "", character = " ") {
return string + intersperse + character.repeat(length - string.length);
}
}