mirror of
https://github.com/barsdeveloper/ueblueprint.git
synced 2026-02-03 23:55:04 +08:00
485 lines
16 KiB
JavaScript
Executable File
485 lines
16 KiB
JavaScript
Executable File
import ComputedType from "./entity/ComputedType.js"
|
|
import Configuration from "./Configuration.js"
|
|
import MirroredEntity from "./entity/MirroredEntity.js"
|
|
import Union from "./entity/Union.js"
|
|
|
|
export default class Utility {
|
|
|
|
static booleanConverter = {
|
|
fromAttribute: (value, type) => {
|
|
value ? "true" : "false"
|
|
},
|
|
toAttribute: (value, type) => {
|
|
if (value === true) {
|
|
return "true"
|
|
}
|
|
if (value === false) {
|
|
return "false"
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
/** @param {Number} x */
|
|
static sigmoid(x, curvature = 1.7) {
|
|
return 1 / (1 + (x / (1 - x) ** -curvature))
|
|
}
|
|
|
|
/** @param {Number} x */
|
|
static sigmoidPositive(x, curvature = 3.7, length = 1.1) {
|
|
return 1 - Math.exp(-((x / length) ** curvature))
|
|
}
|
|
|
|
/** @param {Number} value */
|
|
static clamp(value, min = -Infinity, max = Infinity) {
|
|
return Math.min(Math.max(value, min), max)
|
|
}
|
|
|
|
/** @param {HTMLElement} element */
|
|
static getScale(element) {
|
|
// @ts-expect-error
|
|
const scale = element.blueprint?.getScale() ?? getComputedStyle(element).getPropertyValue("--ueb-scale")
|
|
return scale != "" ? parseFloat(scale) : 1
|
|
}
|
|
|
|
/**
|
|
* @param {Number} num
|
|
* @param {Number} decimals
|
|
*/
|
|
static minDecimals(num, decimals = 1, epsilon = 1e-8) {
|
|
const powered = num * 10 ** decimals
|
|
if (Math.abs(powered % 1) > epsilon) {
|
|
// More decimal digits than required
|
|
return num.toString()
|
|
}
|
|
return num.toFixed(decimals)
|
|
}
|
|
|
|
/** @param {String} value */
|
|
static numberFromText(value = "") {
|
|
value = value.toLowerCase()
|
|
switch (value) {
|
|
case "zero": return 0
|
|
case "one": return 1
|
|
case "two": return 2
|
|
case "three": return 3
|
|
case "four": return 4
|
|
case "five": return 5
|
|
case "six": return 6
|
|
case "seven": return 7
|
|
case "eight": return 8
|
|
case "nine": return 9
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Number} num
|
|
* @param {Number} decimals
|
|
*/
|
|
static roundDecimals(num, decimals = 1) {
|
|
const power = 10 ** decimals
|
|
return Math.round(num * power) / power
|
|
}
|
|
|
|
/** @param {Number} num */
|
|
static printNumber(num) {
|
|
if (num == Number.POSITIVE_INFINITY) {
|
|
return "inf"
|
|
} else if (num == Number.NEGATIVE_INFINITY) {
|
|
return "-inf"
|
|
}
|
|
return Utility.minDecimals(num)
|
|
}
|
|
|
|
/** @param {Number} num */
|
|
static printExponential(num) {
|
|
if (num == Number.POSITIVE_INFINITY) {
|
|
return "inf"
|
|
} else if (num == Number.NEGATIVE_INFINITY) {
|
|
return "-inf"
|
|
}
|
|
const int = Math.round(num)
|
|
if (int >= 1000) {
|
|
const exp = Math.floor(Math.log10(int))
|
|
const dec = Math.round(num / 10 ** (exp - 2)) / 100
|
|
// Not using num.toExponential() because of the omitted leading 0 on the exponential
|
|
return `${dec}e+${exp < 10 ? "0" : ""}${exp}`
|
|
}
|
|
const intPart = Math.floor(num)
|
|
if (intPart == 0) {
|
|
return num.toString()
|
|
}
|
|
return this.roundDecimals(num, Math.max(0, 3 - Math.floor(num).toString().length)).toString()
|
|
}
|
|
|
|
/**
|
|
* @param {Number} a
|
|
* @param {Number} b
|
|
*/
|
|
static approximatelyEqual(a, b, epsilon = 1e-8) {
|
|
return !(Math.abs(a - b) > epsilon)
|
|
}
|
|
|
|
/**
|
|
* @param {Number[]} viewportLocation
|
|
* @param {HTMLElement} movementElement
|
|
*/
|
|
static convertLocation(viewportLocation, movementElement, ignoreScale = false) {
|
|
const scaleCorrection = ignoreScale ? 1 : 1 / Utility.getScale(movementElement)
|
|
const bounding = movementElement.getBoundingClientRect()
|
|
let location = [
|
|
Math.round((viewportLocation[0] - bounding.x) * scaleCorrection),
|
|
Math.round((viewportLocation[1] - bounding.y) * scaleCorrection)
|
|
]
|
|
return location
|
|
}
|
|
|
|
/**
|
|
* @param {IEntity} entity
|
|
* @param {String} key
|
|
* @returns {Boolean}
|
|
*/
|
|
static isSerialized(
|
|
entity,
|
|
key,
|
|
attribute = /** @type {EntityConstructor} */(entity.constructor).attributes?.[key]
|
|
) {
|
|
if (attribute?.constructor === Object) {
|
|
return /** @type {AttributeInformation} */(attribute).serialized
|
|
}
|
|
return false
|
|
}
|
|
|
|
/** @param {String[]} keys */
|
|
static objectGet(target, keys, defaultValue = undefined) {
|
|
if (target === undefined) {
|
|
return undefined
|
|
}
|
|
if (!(keys instanceof Array)) {
|
|
throw new TypeError("UEBlueprint: Expected keys to be an array")
|
|
}
|
|
if (keys.length == 0 || !(keys[0] in target) || target[keys[0]] === undefined) {
|
|
return defaultValue
|
|
}
|
|
if (keys.length == 1) {
|
|
return target[keys[0]]
|
|
}
|
|
return Utility.objectGet(target[keys[0]], keys.slice(1), defaultValue)
|
|
}
|
|
|
|
/**
|
|
* @param {String[]} keys
|
|
* @param {Boolean} create
|
|
* @returns {Boolean}
|
|
*/
|
|
static objectSet(target, keys, value, create = false, defaultDictType = Object) {
|
|
if (!(keys instanceof Array)) {
|
|
throw new TypeError("Expected keys to be an array.")
|
|
}
|
|
if (keys.length == 1) {
|
|
if (create || keys[0] in target || target[keys[0]] === undefined) {
|
|
target[keys[0]] = value
|
|
return true
|
|
}
|
|
} else if (keys.length > 0) {
|
|
if (create && !(target[keys[0]] instanceof Object)) {
|
|
target[keys[0]] = new defaultDictType()
|
|
}
|
|
return Utility.objectSet(target[keys[0]], keys.slice(1), value, create, defaultDictType)
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* @param {AnyValue} a
|
|
* @param {AnyValue} b
|
|
*/
|
|
static equals(a, b) {
|
|
// Here we cannot check both instanceof IEntity because this would introduce a circular include dependency
|
|
if (/** @type {IEntity?} */(a)?.equals && /** @type {IEntity?} */(b)?.equals) {
|
|
return /** @type {IEntity} */(a).equals(/** @type {IEntity} */(b))
|
|
}
|
|
a = Utility.sanitize(a)
|
|
b = Utility.sanitize(b)
|
|
if (a?.constructor === BigInt && b?.constructor === Number) {
|
|
b = BigInt(b)
|
|
} else if (a?.constructor === Number && b?.constructor === BigInt) {
|
|
a = BigInt(a)
|
|
}
|
|
if (a === b) {
|
|
return true
|
|
}
|
|
if (a instanceof Array && b instanceof Array) {
|
|
return a.length === b.length && a.every((value, i) => Utility.equals(value, b[i]))
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* @template {AnyValue} T
|
|
* @param {T} value
|
|
* @returns {SimpleValueType<T>}
|
|
*/
|
|
static getType(value) {
|
|
if (value === null) {
|
|
return null
|
|
}
|
|
if (value?.constructor === Object && /** @type {AttributeInformation} */(value)?.type instanceof Function) {
|
|
// @ts-expect-error
|
|
return /** @type {AttributeInformation} */(value).type
|
|
}
|
|
return /** @type {SimpleValueType<any>} */(value?.constructor)
|
|
}
|
|
|
|
/**
|
|
* @template {SimpleValue} V
|
|
* @template {SimpleValueType<V>} C
|
|
* @param {C} type
|
|
* @returns {value is InstanceType<C>}
|
|
*/
|
|
static isValueOfType(value, type, acceptNull = false) {
|
|
if (type instanceof MirroredEntity) {
|
|
type = type.getTargetType()
|
|
}
|
|
return (acceptNull && value === null) || value instanceof type || value?.constructor === type
|
|
}
|
|
|
|
/** @param {AnyValue | Object} value */
|
|
static sanitize(value, targetType = /** @type {SimpleValueType<typeof value>} */(value?.constructor)) {
|
|
if (targetType instanceof Array) {
|
|
targetType = targetType[0]
|
|
}
|
|
if (targetType instanceof ComputedType) {
|
|
return value // The type is computed, can't say anything about it
|
|
}
|
|
if (targetType instanceof Union) {
|
|
let type = targetType.values.find(t => Utility.isValueOfType(value, t, false))
|
|
if (!type) {
|
|
type = targetType.values[0]
|
|
}
|
|
targetType = type
|
|
}
|
|
if (targetType instanceof MirroredEntity) {
|
|
if (value instanceof MirroredEntity) {
|
|
return value
|
|
}
|
|
return Utility.sanitize(value, targetType.getTargetType())
|
|
}
|
|
if (targetType && !Utility.isValueOfType(value, targetType, true)) {
|
|
value = targetType === BigInt
|
|
? BigInt(/** @type {Number} */(value))
|
|
: new /** @type {EntityConstructor} */(targetType)(value)
|
|
}
|
|
if (value instanceof Boolean || value instanceof Number || value instanceof String) {
|
|
value = /** @type {AnyValue} */(value.valueOf()) // Get the relative primitive value
|
|
}
|
|
return value
|
|
}
|
|
|
|
/**
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {Number} gridSize
|
|
* @returns {[Number, Number]}
|
|
*/
|
|
static snapToGrid(x, y, gridSize) {
|
|
if (gridSize === 1) {
|
|
return [x, y]
|
|
}
|
|
return [
|
|
gridSize * Math.floor(x / gridSize),
|
|
gridSize * Math.floor(y / gridSize)
|
|
]
|
|
}
|
|
|
|
/**
|
|
* @template T
|
|
* @param {Array<T>} a
|
|
* @param {Array<T>} b
|
|
*/
|
|
static mergeArrays(a = [], b = []) {
|
|
let result = []
|
|
a = [...a]
|
|
b = [...b]
|
|
restart:
|
|
while (true) {
|
|
for (let j = 0; j < b.length; ++j) {
|
|
for (let i = 0; i < a.length; ++i) {
|
|
if (a[i] == b[j]) {
|
|
// Found an element in common in the two arrays
|
|
result.push(
|
|
// Take and append all the elements skipped from a
|
|
...a.splice(0, i),
|
|
// Take and append all the elements skippend from b
|
|
...b.splice(0, j),
|
|
// Take and append the element in common
|
|
...a.splice(0, 1)
|
|
)
|
|
b.shift() // Remove the same element from b
|
|
continue restart
|
|
}
|
|
}
|
|
}
|
|
break restart
|
|
}
|
|
// Append remaining the elements in the arrays and make it unique
|
|
return [...(new Set(result.concat(...a, ...b)))]
|
|
}
|
|
|
|
/** @param {String} value */
|
|
static escapeString(value) {
|
|
return value
|
|
.replaceAll(new RegExp(`(${Configuration.stringEscapedCharacters.source})`, "g"), '\\$1')
|
|
.replaceAll("\n", "\\n") // Replace newline with \n
|
|
.replaceAll("\t", "\\t") // Replace tab with \t
|
|
}
|
|
|
|
/** @param {String} value */
|
|
static unescapeString(value) {
|
|
return value
|
|
.replaceAll(new RegExp(Configuration.unescapedBackslash.source + "t", "g"), "\t") // Replace tab with \t
|
|
.replaceAll(new RegExp(Configuration.unescapedBackslash.source + "n", "g"), "\n") // Replace newline with \n
|
|
.replaceAll(new RegExp(`\\\\(${Configuration.stringEscapedCharacters.source})`, "g"), "$1")
|
|
}
|
|
|
|
/** @param {String} value */
|
|
static clearHTMLWhitespace(value) {
|
|
return value
|
|
.replaceAll(" ", "\u00A0") // whitespace
|
|
.replaceAll(/<br\s*\/>|<br>/g, "\n") // newlines
|
|
.replaceAll(/(\<!--.*?\-->)/g, "") // html comments
|
|
}
|
|
|
|
/** @param {String} value */
|
|
static encodeHTMLWhitespace(value) {
|
|
return value.replaceAll(" ", "\u00A0")
|
|
}
|
|
|
|
/** @param {String} value */
|
|
static capitalFirstLetter(value) {
|
|
if (value.length === 0) {
|
|
return value
|
|
}
|
|
return value.charAt(0).toUpperCase() + value.slice(1)
|
|
}
|
|
|
|
/** @param {String} value */
|
|
static formatStringName(value = "") {
|
|
return value
|
|
// Remove leading b (for boolean values) or newlines
|
|
.replace(/^\s*b(?=[A-Z])/, "")
|
|
// Insert a space where needed, possibly removing unnecessary elading characters
|
|
.replaceAll(Configuration.nameRegexSpaceReplacement, " ")
|
|
.trim()
|
|
.split(" ")
|
|
.map(v => Utility.capitalFirstLetter(v))
|
|
.join(" ")
|
|
}
|
|
|
|
/** @param {String} value */
|
|
static getIdFromReference(value) {
|
|
return value
|
|
.replace(/(?:.+\.)?([^\.]+)$/, "$1")
|
|
.replaceAll(/(?<=[a-z\d])(?=[A-Z])|(?<=[a-zA-Z])(?=\d)|(?<=[A-Z]{2})(?=[A-Z][a-z])/g, "-")
|
|
.toLowerCase()
|
|
}
|
|
|
|
/** @param {String} pathValue */
|
|
static getNameFromPath(pathValue) {
|
|
return pathValue.match(/[^\.\/]+$/)?.[0] ?? ""
|
|
}
|
|
|
|
/** @param {LinearColorEntity} value */
|
|
static printLinearColor(value) {
|
|
return `${Math.round(value.R.valueOf() * 255)}, ${Math.round(value.G.valueOf() * 255)}, ${Math.round(value.B.valueOf() * 255)}`
|
|
}
|
|
|
|
/**
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @returns {[Number, Number]}
|
|
*/
|
|
static getPolarCoordinates(x, y, positiveTheta = false) {
|
|
let theta = Math.atan2(y, x)
|
|
if (positiveTheta && theta < 0) {
|
|
theta = 2 * Math.PI + theta
|
|
}
|
|
return [
|
|
Math.sqrt(x * x + y * y),
|
|
theta,
|
|
]
|
|
}
|
|
|
|
/**
|
|
* @param {Number} r
|
|
* @param {Number} theta
|
|
* @returns {[Number, Number]}
|
|
*/
|
|
static getCartesianCoordinates(r, theta) {
|
|
return [
|
|
r * Math.cos(theta),
|
|
r * Math.sin(theta)
|
|
]
|
|
}
|
|
|
|
/**
|
|
* @param {Number} begin
|
|
* @param {Number} end
|
|
*/
|
|
static range(begin = 0, end = 0, step = end >= begin ? 1 : -1) {
|
|
return Array.from({ length: Math.ceil((end - begin) / step) }, (_, i) => begin + (i * step))
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} element
|
|
* @param {String} value
|
|
*/
|
|
static paste(element, value) {
|
|
const event = new ClipboardEvent("paste", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clipboardData: new DataTransfer(),
|
|
})
|
|
event.clipboardData.setData("text", value)
|
|
element.dispatchEvent(event)
|
|
}
|
|
|
|
/** @param {Blueprint} blueprint */
|
|
static async copy(blueprint) {
|
|
const event = new ClipboardEvent("copy", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clipboardData: new DataTransfer(),
|
|
})
|
|
blueprint.dispatchEvent(event)
|
|
}
|
|
|
|
static animate(
|
|
from,
|
|
to,
|
|
intervalSeconds,
|
|
callback,
|
|
acknowledgeRequestAnimationId = id => { },
|
|
timingFunction = x => {
|
|
const v = x ** 3.5
|
|
return v / (v + ((1 - x) ** 3.5))
|
|
}
|
|
) {
|
|
let startTimestamp
|
|
const doAnimation = currentTimestamp => {
|
|
if (startTimestamp === undefined) {
|
|
startTimestamp = currentTimestamp
|
|
}
|
|
let delta = (currentTimestamp - startTimestamp) / intervalSeconds
|
|
if (Utility.approximatelyEqual(delta, 1) || delta > 1) {
|
|
delta = 1
|
|
} else {
|
|
acknowledgeRequestAnimationId(requestAnimationFrame(doAnimation))
|
|
}
|
|
const currentValue = from + (to - from) * timingFunction(delta)
|
|
callback(currentValue)
|
|
}
|
|
acknowledgeRequestAnimationId(requestAnimationFrame(doAnimation))
|
|
}
|
|
}
|