import * as uuid from 'uuid'; export function clamp(n: number, lower: number, upper: number) { return Math.max(lower, Math.min(upper, n)); } export function ptDistance(p1: { x: number; y: number }, p2: { x: number; y: number }) { return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2); } export namespace Utils { export function GuestID() { return '__guest__'; } export function GenerateGuid(): string { return uuid.v4(); } export function GenerateDeterministicGuid(seed: string): string { return uuid.v5(seed, uuid.v5.URL); } export const loggingEnabled = false; export const logFilter: number | undefined = undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function log(prefixIn: string, messageName: string, messageIn: any, receiving: boolean) { let prefix = prefixIn; let message = messageIn; if (!loggingEnabled) { return; } message = message || {}; if (logFilter !== undefined && logFilter !== message.type) { return; } const idString = (message.id || '').padStart(36, ' '); prefix = prefix.padEnd(16, ' '); console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)} `); } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function loggingCallback(prefix: string, func: (args: any) => void, messageName: string) { return (args: unknown) => { log(prefix, messageName, args, true); func(args); }; } export function TraceConsoleLog() { ['log', 'warn'].forEach(method => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const old = (console as any)[method]; // eslint-disable-next-line @typescript-eslint/no-explicit-any (console as any)[method] = function (...args: any[]) { let stack = new Error('').stack?.split(/\n/); // Chrome includes a single "Error" line, FF doesn't. if (stack && stack[0].indexOf('Error') === 0) { stack = stack.slice(1); } const message = (stack?.[1] || 'Stack undefined!').trim(); const newArgs = args.slice().concat([message]); return old.apply(console, newArgs); }; }); } export function rotPt(x: number, y: number, radAng: number) { return { x: x * Math.cos(radAng) - y * Math.sin(radAng), y: x * Math.sin(radAng) + y * Math.cos(radAng) }; } export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, xIn: number, yIn: number) { const r = l + w; const b = t + h; const x = clamp(xIn, l, r); const y = clamp(yIn, t, b); const dl = Math.abs(x - l); const dr = Math.abs(x - r); const dt = Math.abs(y - t); const db = Math.abs(y - b); const m = Math.min(dl, dr, dt, db); return m === dt ? [x, t] : m === db ? [x, b] : m === dl ? [l, y] : [r, y]; } } export function decimalToHexString(numberIn: number) { const number = numberIn < 0 ? 0xffffffff + numberIn + 1 : numberIn; return (number < 16 ? '0' : '') + number.toString(16).toUpperCase(); } export function distanceBetweenHorizontalLines(xs: number, xe: number, y: number, xs2: number, xe2: number, y2: number): [number, number[]] { if ((xs2 <= xs && xe2 >= xs) || (xs2 <= xe && xe2 >= xe) || (xs2 >= xs && xe2 <= xe)) return [Math.abs(y - y2), [Math.max(xs, xs2), y, Math.min(xe, xe2), y]]; if (xe2 <= xs) return [Math.sqrt((xe2 - xs) * (xe2 - xs) + (y2 - y) * (y2 - y)), [xs, y, xs, y]]; // if (xs2 > xe) return [Math.sqrt((xs2 - xe) * (xs2 - xe) + (y2 - y) * (y2 - y)), [xe, y, xe, y]]; } export function distanceBetweenVerticalLines(x: number, ys: number, ye: number, x2: number, ys2: number, ye2: number): [number, number[]] { if ((ys2 <= ys && ye2 >= ys) || (ys2 <= ye && ye2 >= ye) || (ys2 >= ys && ye2 <= ye)) return [Math.abs(x - x2), [x, Math.max(ys, ys2), x, Math.min(ye, ye2)]]; if (ye2 <= ys) return [Math.sqrt((ye2 - ys) * (ye2 - ys) + (x2 - x) * (x2 - x)), [x, ys, x, ys]]; // if (ys2 > ye) return [Math.sqrt((ys2 - ye) * (ys2 - ye) + (x2 - x) * (x2 - x)), [x, ye, x, ye]]; } function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) { if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 }; const atob = { x: bx - ax, y: by - ay }; const atop = { x: px - ax, y: py - ay }; const len = atob.x * atob.x + atob.y * atob.y; let dot = atop.x * atob.x + atop.y * atob.y; const t = Math.min(1, Math.max(0, dot / len)); dot = (bx - ax) * (py - ay) - (by - ay) * (px - ax); return { point: { x: ax + atob.x * t, y: ay + atob.y * t, }, left: dot < 1, dot: dot, t: t, }; } export function closestPtBetweenRectangles(l: number, t: number, w: number, h: number, l1: number, t1: number, w1: number, h1: number, x: number, y: number) { const r = l + w; const b = t + h; const r1 = l1 + w1; const b1 = t1 + h1; const hsegs = [ [l, r, t, l1, r1, t1], [l, r, b, l1, r1, t1], [l, r, t, l1, r1, b1], [l, r, b, l1, r1, b1], ]; const vsegs = [ [l, t, b, l1, t1, b1], [r, t, b, l1, t1, b1], [l, t, b, r1, t1, b1], [r, t, b, r1, t1, b1], ]; const res = hsegs.reduce( (closest, seg) => { const dist = distanceBetweenHorizontalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]); return dist[0] < closest[0] ? dist : closest; }, [Number.MAX_VALUE, []] as [number, number[]] ); const fres = vsegs.reduce((closest, seg) => { const dist = distanceBetweenVerticalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]); return dist[0] < closest[0] ? dist : closest; }, res); const near = project(x, y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]); return project(near.point.x, near.point.y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]); } export function timenow() { const now = new Date(); let ampm = 'am'; let h = now.getHours(); let m: string | number = now.getMinutes(); if (h >= 12) { if (h > 12) h -= 12; ampm = 'pm'; } if (m < 10) m = '0' + m; return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm; } export function formatTime(timeIn: number) { const time = Math.round(timeIn); const hours = Math.floor(time / 60 / 60); const minutes = Math.floor(time / 60) - hours * 60; const seconds = time % 60; return (hours ? hours.toString() + ':' : '') + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); } // x is furthest left, y is furthest top, r is furthest right, b is furthest bottom export function aggregateBounds(boundsList: { x: number; y: number; width?: number; height?: number }[], xpad: number, ypad: number) { const bounds = boundsList .map(b => ({ x: b.x, y: b.y, r: b.x + (b.width || 0), b: b.y + (b.height || 0) })) .reduce( (prevBounds, b) => ({ x: Math.min(b.x, prevBounds.x), y: Math.min(b.y, prevBounds.y), r: Math.max(b.r, prevBounds.r), b: Math.max(b.b, prevBounds.b), }), { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE } ); return { x: bounds.x !== Number.MAX_VALUE ? bounds.x - xpad : bounds.x, y: bounds.y !== Number.MAX_VALUE ? bounds.y - ypad : bounds.y, r: bounds.r !== -Number.MAX_VALUE ? bounds.r + xpad : bounds.r, b: bounds.b !== -Number.MAX_VALUE ? bounds.b + ypad : bounds.b, }; } export function intersectRect(r1: { left: number; top: number; width: number; height: number }, r2: { left: number; top: number; width: number; height: number }) { return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); } export function stringHash(s?: string) { return !s ? undefined : Math.abs(s.split('').reduce((a, b) => (n => n & n)((a << 5) - a + b.charCodeAt(0)), 0)); } export function percent2frac(percent: string) { return Number(percent.substr(0, percent.length - 1)) / 100; } export type Without = Pick>; export type Predicate = (entry: [K, V]) => boolean; /** * creates a list of numbers ordered from 0 to 'num' * @param num range of numbers * @returns list of values from 0 to num -1 */ export function numberRange(num: number) { return num > 0 && num < 1000 ? Array.from(Array(num)).map((v, i) => i) : []; } export function emptyFunction() { return undefined; } export function unimplementedFunction() { throw new Error('This function is not implemented, but should be.'); } export function DeepCopy(source: Map, predicate?: Predicate) { const deepCopy = new Map(); const entries = source.entries(); let next = entries.next(); while (!next.done) { const entry = next.value; if (!predicate || predicate(entry)) { deepCopy.set(entry[0], entry[1]); } next = entries.next(); } return deepCopy; } export namespace JSONUtils { export function tryParse(source: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let results: any; try { results = JSON.parse(source); } catch (e) { console.log('JSONparse error: ', e); results = source; } return results; } } /** * Helper method for converting pixel string eg. '32px' into number eg. 32 * @param value: string with 'px' ending * @returns value: number * * Example: * '32px' -> 32 */ export function numberValue(value: string | undefined): number { if (value === undefined) return 0; return parseInt(value); } export function numbersAlmostEqual(num1: number, num2: number) { return Math.abs(num1 - num2) < 0.2; }