} parser */
constructor(parser) {
super();
this.#parser = parser;
}
/**
* @param {PathNode} path
* @param {Number} index
*/
makePath(path, index) {
return path
}
/**
* @param {Context} context
* @param {PathNode} path
*/
isHighlighted(context, path) {
if (super.isHighlighted(context, path)) {
// If LazyParser is highlighted, then highlight its child
const childrenPath = { parent: path, parser: this.#resolvedPraser, index: 0 };
context.highlighted = context.highlighted instanceof Parser ? this.#resolvedPraser : childrenPath;
}
return false
}
resolve() {
if (!this.#resolvedPraser) {
this.#resolvedPraser = this.#parser().getParser();
}
return this.#resolvedPraser
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
*/
parse(context, position, path, index) {
this.resolve();
this.parse = this.#resolvedPraser.parse.bind(this.#resolvedPraser);
return this.parse(context, position, path, index)
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
this.resolve();
this.doToString = this.#resolvedPraser.toString.bind(this.#resolvedPraser);
return this.doToString(context, indentation, path, index)
}
}
/** @template {Parser} T */
class Lookahead extends Parser {
#parser
get parser() {
return this.#parser
}
#type
get type() {
return this.#type
}
/**
* @readonly
* @enum {String}
*/
static Type = {
NEGATIVE_AHEAD: "?!",
NEGATIVE_BEHIND: "? String.raw`[^${character}\\]*(?:\\.[^${character}\\]*)*`
static #numberRegex = /[-\+]?(?:\d*\.)?\d+/
static common = {
number: new RegExp(this.#numberRegex.source + String.raw`(?!\.)`),
numberInteger: /[\-\+]?\d+(?!\.\d)/,
numberNatural: /\d+/,
numberExponential: new RegExp(this.#numberRegex.source + String.raw`(?:[eE][\+\-]?\d+)?(?!\.)`),
numberUnit: /\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/,
numberByte: /0*(?:25[0-5]|2[0-4]\d|1?\d?\d)(?!\d|\.)/,
whitespace: /\s+/,
whitespaceOpt: /\s*/,
whitespaceInline: /[^\S\n]+/,
whitespaceInlineOpt: /[^\S\n]*/,
whitespaceMultiline: /\s*?\n\s*/,
doubleQuotedString: new RegExp(`"(${this.#createEscapeable('"')})"`),
singleQuotedString: new RegExp(`'(${this.#createEscapeable("'")})'`),
backtickQuotedString: new RegExp("`(" + this.#createEscapeable("`") + ")`"),
}
/**
* @param {RegExp} regexp
* @param {(match: RegExpExecArray) => T} matchMapper
*/
constructor(regexp, matchMapper) {
super();
this.#regexp = regexp;
this.#anchoredRegexp = new RegExp(`^(?:${regexp.source})`, regexp.flags);
this.#matchMapper = matchMapper;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const match = this.#anchoredRegexp.exec(context.input.substring(position));
if (match) {
position += match[0].length;
}
const result = match
? Reply.makeSuccess(position, this.#matchMapper(match), path, position)
: Reply.makeFailure();
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
let result = "/" + this.#regexp.source + "/";
const shortname = Object.entries(RegExpParser.common).find(([k, v]) => v.source === this.#regexp.source)?.[0];
if (shortname) {
result = "P." + shortname;
}
return result
}
}
/**
* @template {Parser} T
* @template P
*/
class MapParser extends Parser {
#parser
get parser() {
return this.#parser
}
#mapper
get mapper() {
return this.#mapper
}
/**
* @param {T} parser
* @param {(v: ParserValue) => P} mapper
*/
constructor(parser, mapper) {
super();
this.#parser = parser;
this.#mapper = mapper;
}
/**
* @param {Context} context
* @param {PathNode} path
*/
isHighlighted(context, path) {
if (super.isHighlighted(context, path)) {
// If MapParser is highlighted, then highlight its child
const childrenPath = { parent: path, parser: this.#parser, index: 0 };
context.highlighted = context.highlighted instanceof Parser ? this.#parser : childrenPath;
}
return false
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result
}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const result = this.#parser.parse(context, position, path, 0);
if (result.status) {
result.value = this.#mapper(result.value);
}
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
let result = this.#parser.toString(context, indentation, path, 0);
if (this.#parser instanceof RegExpParser) {
if (Object.values(RegExpParser.common).includes(this.#parser.regexp)) {
if (
this.#parser.regexp === RegExpParser.common.numberInteger
&& this.#mapper === /** @type {(v: any) => BigInt} */(BigInt)
) {
return "P.numberBigInteger"
}
return result
}
}
let serializedMapper = this.#mapper.toString();
if (serializedMapper.length > 60 || serializedMapper.includes("\n")) {
serializedMapper = "(...) => { ... }";
}
result += ` -> map<${serializedMapper}>`;
return result
}
}
/** @extends {RegExpParser} */
class RegExpArrayParser extends RegExpParser {
/** @param {RegExpExecArray} match */
static #mapper = match => match
/** @param {RegExp} regexp */
constructor(regexp) {
super(regexp, RegExpArrayParser.#mapper);
}
}
/** @extends {RegExpParser} */
class RegExpValueParser extends RegExpParser {
/** @param {RegExp} regexp */
constructor(regexp, group = 0) {
super(
regexp,
/** @param {RegExpExecArray} match */
match => match[group]
);
}
}
/** @template {Parser[]} T */
class SequenceParser extends Parser {
#parsers
get parsers() {
return this.#parsers
}
/** @param {T} parsers */
constructor(...parsers) {
super();
this.#parsers = parsers;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const value = /** @type {ParserValue} */(new Array(this.#parsers.length));
const result = Reply.makeSuccess(position, value);
for (let i = 0; i < this.#parsers.length; ++i) {
const outcome = this.#parsers[i].parse(context, result.position, path, i);
if (outcome.bestPosition > result.bestPosition) {
result.bestParser = outcome.bestParser;
result.bestPosition = outcome.bestPosition;
}
if (!outcome.status) {
result.status = false;
result.value = null;
break
}
result.value[i] = outcome.value;
result.position = outcome.position;
}
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
const deeperIndentation = indentation + Parser.indentation;
const result = "SEQ<\n"
+ deeperIndentation
+ this.#parsers
.map((parser, index) => parser.toString(context, deeperIndentation, path, index))
.join("\n" + deeperIndentation)
+ "\n" + indentation + ">";
return result
}
}
/** @template {Parser} T */
class TimesParser extends Parser {
#parser
get parser() {
return this.#parser
}
#min
get min() {
return this.#min
}
#max
get max() {
return this.#max
}
/** @param {T} parser */
constructor(parser, min = 0, max = Number.POSITIVE_INFINITY) {
super();
if (min > max) {
throw new Error("Min is greater than max")
}
this.#parser = parser;
this.#min = min;
this.#max = max;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const value = /** @type {ParserValue[]} */([]);
const result = Reply.makeSuccess(position, value, path);
for (let i = 0; i < this.#max; ++i) {
const outcome = this.#parser.parse(context, result.position, path, 0);
if (outcome.bestPosition > result.bestPosition) {
result.bestParser = outcome.bestParser;
result.bestPosition = outcome.bestPosition;
}
if (!outcome.status) {
if (i < this.#min) {
result.status = false;
result.value = null;
}
break
}
result.value.push(outcome.value);
result.position = outcome.position;
}
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
let result = this.parser.toString(context, indentation, path, 0);
const serialized =
this.#min === 0 && this.#max === 1 ? "?"
: this.#min === 0 && this.#max === Number.POSITIVE_INFINITY ? "*"
: this.#min === 1 && this.#max === Number.POSITIVE_INFINITY ? "+"
: "{"
+ this.#min
+ (this.#min !== this.#max ? "," + this.#max : "")
+ "}";
result += serialized;
return result
}
}
/** @template {Parser} T */
class Parsernostrum {
#parser
/** @type {(new (parser: Parser) => Parsernostrum) & typeof Parsernostrum} */
Self
static lineColumnFromOffset(string, offset) {
const lines = string.substring(0, offset).split('\n');
const line = lines.length;
const column = lines[lines.length - 1].length + 1;
return { line, column }
}
/** @param {[any, ...any]|RegExpExecArray} param0 */
static #firstElementGetter = ([v, _]) => v
/** @param {[any, any, ...any]|RegExpExecArray} param0 */
static #secondElementGetter = ([_, v]) => v
static #arrayFlatter = ([first, rest]) => [first, ...rest]
/**
* @template T
* @param {T} v
* @returns {T extends Array ? String : T}
*/
// @ts-expect-error
static #joiner = v => v instanceof Array ? v.join("") : v
static #createEscapeable = character => String.raw`[^${character}\\]*(?:\\.[^${character}\\]*)*`
// Prefedined parsers
/** Parser accepting any valid decimal, possibly signed number */
static number = this.reg(RegExpParser.common.number).map(Number)
/** Parser accepting any digits only number */
static numberInteger = this.reg(RegExpParser.common.numberInteger).map(Number)
/** Parser accepting any digits only number and returns a BigInt */
static numberBigInteger = this.reg(this.numberInteger.getParser().parser.regexp).map(BigInt)
/** Parser accepting any digits only number */
static numberNatural = this.reg(RegExpParser.common.numberNatural).map(Number)
/** Parser accepting any valid decimal, possibly signed, possibly in the exponential form number */
static numberExponential = this.reg(RegExpParser.common.numberExponential).map(Number)
/** Parser accepting any valid decimal number between 0 and 1 */
static numberUnit = this.reg(RegExpParser.common.numberUnit).map(Number)
/** Parser accepting any integer between 0 and 255 */
static numberByte = this.reg(RegExpParser.common.numberByte).map(Number)
/** Parser accepting whitespace */
static whitespace = this.reg(RegExpParser.common.whitespace)
/** Parser accepting whitespace */
static whitespaceOpt = this.reg(RegExpParser.common.whitespaceOpt)
/** Parser accepting whitespace that spans on a single line */
static whitespaceInline = this.reg(RegExpParser.common.whitespaceInline)
/** Parser accepting whitespace that spans on a single line */
static whitespaceInlineOpt = this.reg(RegExpParser.common.whitespaceInlineOpt)
/** Parser accepting whitespace that contains a list a newline */
static whitespaceMultiline = this.reg(RegExpParser.common.whitespaceMultiline)
/** Parser accepting a double quoted string and returns the content */
static doubleQuotedString = this.reg(RegExpParser.common.doubleQuotedString, 1)
/** Parser accepting a single quoted string and returns the content */
static singleQuotedString = this.reg(RegExpParser.common.singleQuotedString, 1)
/** Parser accepting a backtick quoted string and returns the content */
static backtickQuotedString = this.reg(RegExpParser.common.backtickQuotedString, 1)
/** @param {T} parser */
constructor(parser, optimized = false) {
this.#parser = parser;
}
/** @param {PathNode} path */
static #simplifyPath(path) {
/** @type {PathNode[]} */
const array = [];
while (path) {
array.push(path);
path = path.parent;
}
array.reverse();
/** @type {Map} */
let visited = new Map();
for (let i = 1; i < array.length; ++i) {
const existing = visited.get(array[i].current);
if (existing !== undefined) {
if (array[i + 1]) {
array[i + 1].parent = array[existing];
}
visited = new Map([...visited.entries()].filter(([parser, index]) => index <= existing || index > i));
visited.set(array[i].current, existing);
array.splice(existing + 1, i - existing);
i = existing;
} else {
visited.set(array[i].current, i);
}
}
return array[array.length - 1]
}
getParser() {
return this.#parser
}
/**
* @param {String} input
* @returns {Result>}
*/
run(input) {
const result = this.#parser.parse(Reply.makeContext(this, input), 0, Reply.makePathNode(), 0);
if (result.position !== input.length) {
result.status = false;
}
return result
}
/**
* @param {String} input
* @throws {Error} when the parser fails to match
*/
parse(input, printParser = true) {
const result = this.run(input);
if (result.status) {
return result.value
}
const chunkLength = 60;
const chunkRange = /** @type {[Number, Number]} */(
[Math.ceil(chunkLength / 2), Math.floor(chunkLength / 2)]
);
const position = Parsernostrum.lineColumnFromOffset(input, result.bestPosition);
let bestPosition = result.bestPosition;
const inlineInput = input.replaceAll(
/^(\s)+|\s{6,}|\s*?\n\s*/g,
(m, startingSpace, offset) => {
let replaced = startingSpace ? "..." : " ... ";
if (offset <= result.bestPosition) {
if (result.bestPosition < offset + m.length) {
bestPosition -= result.bestPosition - offset;
} else {
bestPosition -= m.length - replaced.length;
}
}
return replaced
}
);
const string = inlineInput.substring(0, chunkLength).trimEnd();
const leadingWhitespaceLength = Math.min(
input.substring(result.bestPosition - chunkRange[0]).match(/^\s*/)[0].length,
chunkRange[0] - 1,
);
let offset = Math.min(bestPosition, chunkRange[0] - leadingWhitespaceLength);
chunkRange[0] = Math.max(0, bestPosition - chunkRange[0]) + leadingWhitespaceLength;
chunkRange[1] = Math.min(input.length, chunkRange[0] + chunkLength);
let segment = inlineInput.substring(...chunkRange);
if (chunkRange[0] > 0) {
segment = "..." + segment;
offset += 3;
}
if (chunkRange[1] < inlineInput.length - 1) {
segment = segment + "...";
}
const bestParser = this.toString(Parser.indentation, true, Parsernostrum.#simplifyPath(result.bestParser));
throw new Error(
`Could not parse: ${string}\n\n`
+ `Input: ${segment}\n`
+ " " + " ".repeat(offset)
+ `^ From here (line: ${position.line}, `
+ `column: ${position.column}, `
+ `offset: ${result.bestPosition})${result.bestPosition === input.length ? ", end of string" : ""}\n`
+ (printParser
? "\n"
+ (result.bestParser ? "Last valid parser matched:" : "No parser matched:")
+ bestParser
+ "\n"
: ""
)
)
}
// Parsers
/**
* @template {String} S
* @param {S} value
*/
static str(value) {
return new this(new StringParser(value))
}
/** @param {RegExp} value */
static reg(value, group = 0) {
return new this(new RegExpValueParser(value, group))
}
/** @param {RegExp} value */
static regArray(value) {
return new this(new RegExpArrayParser(value))
}
static success() {
return new this(SuccessParser.instance)
}
static failure() {
return new this(FailureParser.instance)
}
// Combinators
/**
* @template {[Parsernostrum, Parsernostrum, ...Parsernostrum[]]} P
* @param {P} parsers
* @returns {Parsernostrum>>}
*/
static seq(...parsers) {
const results = new this(new SequenceParser(...parsers.map(p => p.getParser())));
// @ts-expect-error
return results
}
/**
* @template {Parsernostrum[]} P
* @param {P} parsers
* @returns {Parsernostrum>>}
*/
static alt(...parsers) {
// @ts-expect-error
return new this(new AlternativeParser(...parsers.map(p => p.getParser())))
}
/**
* @template {Parsernostrum} P
* @param {P} parser
*/
static lookahead(parser) {
return new this(new Lookahead(parser.getParser(), Lookahead.Type.POSITIVE_AHEAD))
}
/**
* @template {Parsernostrum} P
* @param {() => P} parser
* @returns {Parsernostrum>>}
*/
static lazy(parser) {
return new this(new LazyParser(parser))
}
/** @param {Number} min */
times(min, max = min) {
return new Parsernostrum(new TimesParser(this.#parser, min, max))
}
many() {
return this.times(0, Number.POSITIVE_INFINITY)
}
/** @param {Number} n */
atLeast(n) {
return this.times(n, Number.POSITIVE_INFINITY)
}
/** @param {Number} n */
atMost(n) {
return this.times(0, n)
}
/** @returns {Parsernostrum} */
opt() {
// @ts-expect-error
return Parsernostrum.alt(this, Parsernostrum.success())
}
/**
* @template {Parsernostrum} P
* @param {P} separator
*/
sepBy(separator, allowTrailing = false) {
const results = Parsernostrum.seq(
this,
Parsernostrum.seq(separator, this).map(Parsernostrum.#secondElementGetter).many()
)
.map(Parsernostrum.#arrayFlatter);
return results
}
skipSpace() {
return Parsernostrum.seq(this, Parsernostrum.whitespaceOpt).map(Parsernostrum.#firstElementGetter)
}
/**
* @template P
* @param {(v: ParserValue) => P} fn
* @returns {Parsernostrum>}
*/
map(fn) {
// @ts-expect-error
return new Parsernostrum(new MapParser(this.#parser, fn))
}
/**
* @template {Parsernostrum} P
* @param {(v: ParserValue, input: String, position: Number) => P} fn
*/
chain(fn) {
return new Parsernostrum(new ChainedParser(this.#parser, fn))
}
/**
* @param {(v: ParserValue, input: String, position: Number) => boolean} fn
* @return {Parsernostrum}
*/
assert(fn) {
// @ts-expect-error
return this.chain((v, input, position) => fn(v, input, position)
? Parsernostrum.success().map(() => v)
: Parsernostrum.failure()
)
}
join(value = "") {
return this.map(Parsernostrum.#joiner)
}
label(value = "") {
return new Parsernostrum(new Label(this.#parser, value))
}
/** @param {Parsernostrum | Parser | PathNode} highlight */
toString(indentation = "", newline = false, highlight = null) {
if (highlight instanceof Parsernostrum) {
highlight = highlight.getParser();
}
const context = Reply.makeContext(this, "");
context.highlighted = highlight;
const path = Reply.makePathNode();
return (newline ? "\n" + indentation : "") + this.#parser.toString(context, indentation, path)
}
}
class Serializable {
static grammar = this.createGrammar()
/** @protected */
static createGrammar() {
return /** @type {Parsernostrum} */(Parsernostrum.failure())
}
}
class SerializerFactory {
static #serializers = new Map()
/**
* @template {AttributeConstructor} T
* @param {T} type
* @param {Serializer} object
*/
static registerSerializer(type, object) {
SerializerFactory.#serializers.set(type, object);
}
/**
* @template {AttributeConstructor} T
* @param {T} type
* @returns {Serializer}
*/
static getSerializer(type) {
return SerializerFactory.#serializers.get(type)
}
}
/** @abstract */
class IEntity extends Serializable {
/** @type {{ [attribute: String]: AttributeInfo }} */
static attributes = {
attributes: new AttributeInfo({
ignored: true,
}),
lookbehind: new AttributeInfo({
default: /** @type {String | Union} */(""),
ignored: true,
uninitialized: true,
}),
}
/** @type {String[]} */
#_keys
get _keys() {
return this.#_keys
}
set _keys(keys) {
this.#_keys = keys;
}
constructor(values = {}, suppressWarns = false) {
super();
const Self = /** @type {typeof IEntity} */(this.constructor);
/** @type {AttributeDeclarations?} */ this.attributes;
/** @type {String} */ this.lookbehind;
const valuesKeys = Object.keys(values);
const attributesKeys = values.attributes
? Utility.mergeArrays(Object.keys(values.attributes), Object.keys(Self.attributes))
: Object.keys(Self.attributes);
const allAttributesKeys = Utility.mergeArrays(valuesKeys, attributesKeys);
for (const key of allAttributesKeys) {
let value = values[key];
if (!suppressWarns && !(key in values)) {
if (!(key in Self.attributes) && !key.startsWith(Configuration.subObjectAttributeNamePrefix)) {
const typeName = value instanceof Array ? `[${value[0]?.constructor.name}]` : value.constructor.name;
console.warn(
`UEBlueprint: Attribute ${key} (of type ${typeName}) in the serialized data is not defined in ${Self.name}.attributes`
);
}
}
if (!(key in Self.attributes)) {
// Remember attributeName can come from the values and be not defined in the attributes.
// In that case just assign it and skip the rest.
this[key] = value;
continue
}
Self.attributes.lookbehind;
const predicate = AttributeInfo.getAttribute(values, key, "predicate", Self);
const assignAttribute = !predicate
? v => this[key] = v
: v => {
Object.defineProperties(this, {
["#" + key]: {
writable: true,
enumerable: false,
},
[key]: {
enumerable: true,
get() {
return this["#" + key]
},
set(v) {
if (!predicate(v)) {
console.warn(
`UEBlueprint: Tried to assign attribute ${key} to ${Self.name} not satisfying the predicate`
);
return
}
this["#" + key] = v;
}
},
});
this[key] = v;
};
let defaultValue = AttributeInfo.getAttribute(values, key, "default", Self);
if (defaultValue instanceof Function) {
defaultValue = defaultValue(this);
}
let defaultType = AttributeInfo.getAttribute(values, key, "type", Self);
if (defaultType instanceof ComputedType) {
defaultType = defaultType.compute(this);
}
if (defaultType instanceof Array) {
defaultType = Array;
}
if (defaultType === undefined) {
defaultType = Utility.getType(defaultValue);
}
if (value !== undefined) {
// Remember value can still be null
if (
value?.constructor === String
&& AttributeInfo.getAttribute(values, key, "serialized", Self)
&& defaultType !== String
) {
try {
value = SerializerFactory
.getSerializer(defaultType)
.read(/** @type {String} */(value));
} catch (e) {
assignAttribute(value);
continue
}
}
assignAttribute(Utility.sanitize(value, /** @type {AttributeConstructor} */(defaultType)));
continue // We have a value, need nothing more
}
if (defaultValue !== undefined && !AttributeInfo.getAttribute(values, key, "uninitialized", Self)) {
assignAttribute(defaultValue);
}
}
}
/** @param {AttributeTypeDescription} attributeType */
static defaultValueProviderFromType(attributeType) {
if (attributeType === Boolean) {
return false
} else if (attributeType === Number) {
return 0
} else if (attributeType === BigInt) {
return 0n
} else if (attributeType === String) {
return ""
} else if (attributeType === Array || attributeType instanceof Array) {
return () => []
} else if (attributeType instanceof Union) {
return this.defaultValueProviderFromType(attributeType.values[0])
} else if (attributeType instanceof MirroredEntity) {
return () => new MirroredEntity(attributeType.type, attributeType.getter)
} else if (attributeType instanceof ComputedType) {
return undefined
} else {
return () => new /** @type {AnyConstructor} */(attributeType)()
}
}
/**
* @template {new (...args: any) => any} C
* @param {C} type
* @returns {value is InstanceType}
*/
static isValueOfType(value, type) {
return value != null && (value instanceof type || value.constructor === type)
}
static defineAttributes(object, attributes) {
Object.defineProperty(object, "attributes", {
writable: true,
configurable: false,
});
object.attributes = attributes;
}
/**
*
* @param {String} attribute
* @param {(v: any) => void} callback
*/
listenAttribute(attribute, callback) {
const descriptor = Object.getOwnPropertyDescriptor(this, attribute);
const setter = descriptor.set;
if (setter) {
descriptor.set = v => {
setter(v);
callback(v);
};
Object.defineProperties(this, { [attribute]: descriptor });
} else if (descriptor.value) {
Object.defineProperties(this, {
["#" + attribute]: {
value: descriptor.value,
writable: true,
enumerable: false,
},
[attribute]: {
enumerable: true,
get() {
return this["#" + attribute]
},
set(v) {
if (v == this["#" + attribute]) {
return
}
callback(v);
this["#" + attribute] = v;
}
},
});
}
}
getLookbehind() {
let lookbehind = this.lookbehind ?? AttributeInfo.getAttribute(this, "lookbehind", "default");
lookbehind = lookbehind instanceof Union ? lookbehind.values[0] : lookbehind;
return lookbehind
}
unexpectedKeys() {
return Object.keys(this).length - Object.keys(/** @type {typeof IEntity} */(this.constructor).attributes).length
}
/** @param {IEntity} other */
equals(other) {
const thisKeys = Object.keys(this);
const otherKeys = Object.keys(other);
if (thisKeys.length != otherKeys.length) {
return false
}
for (const key of thisKeys) {
if (this[key] instanceof IEntity && !this[key].equals(other[key])) {
return false
} else if (!Utility.equals(this[key], other[key])) {
return false
}
}
return true
}
}
class Grammar {
static separatedBy = (source, separator, min = 1) =>
new RegExp(
source + "(?:" + separator + source + ")"
+ (min === 1 ? "*" : min === 2 ? "+" : `{${min},}`)
)
static Regex = class {
static HexDigit = /[0-9a-fA-F]/
static InsideString = /(?:[^"\\]|\\.)*/
static InsideSingleQuotedString = /(?:[^'\\]|\\.)*/
static Integer = /[\-\+]?\d+(?!\d|\.)/
static Number = /[-\+]?(?:\d*\.)?\d+(?!\d|\.)/
static RealUnit = /\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/ // A number between 0 and 1 included
static Word = Grammar.separatedBy("[a-zA-Z]", "_")
static Symbol = /[a-zA-Z_]\w*/
static DotSeparatedSymbols = Grammar.separatedBy(this.Symbol.source, "\\.")
static MultipleWordsSymbols = Grammar.separatedBy(this.Symbol.source, "(?:\\.|\\ +)")
static PathFragment = Grammar.separatedBy(this.Symbol.source, "[\\.:]")
static PathSpaceFragment = Grammar.separatedBy(this.Symbol.source, "[\\.:\\ ]")
static Path = new RegExp(`(?:\\/${this.PathFragment.source}){2,}`) // Multiple (2+) /PathFragment
}
/* --- Primitive --- */
static null = Parsernostrum.reg(/\(\s*\)/).map(() => null)
static true = Parsernostrum.reg(/true/i).map(() => true)
static false = Parsernostrum.reg(/false/i).map(() => false)
static boolean = Parsernostrum.regArray(/(true)|false/i).map(v => v[1] ? true : false)
static number = Parsernostrum.regArray(
new RegExp(`(${Parsernostrum.number.getParser().parser.regexp.source})|(\\+?inf)|(-inf)`)
).map(([_0, n, plusInf, minusInf]) => n ? Number(n) : plusInf ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY)
static bigInt = Parsernostrum.reg(new RegExp(Parsernostrum.number.getParser().parser.regexp.source)).map(BigInt)
.map(result =>
result[2] !== undefined
? Number.POSITIVE_INFINITY
: result[3] !== undefined
? Number.NEGATIVE_INFINITY
: Number(result[1])
)
static naturalNumber = Parsernostrum.lazy(() => Parsernostrum.reg(/\d+/).map(Number))
static string = Parsernostrum.doubleQuotedString.map(insideString => Utility.unescapeString(insideString))
/* --- Fragment --- */
static colorValue = Parsernostrum.numberByte
static word = Parsernostrum.reg(Grammar.Regex.Word)
static symbol = Parsernostrum.reg(Grammar.Regex.Symbol)
static symbolQuoted = Parsernostrum.reg(new RegExp('"(' + Grammar.Regex.Symbol.source + ')"'), 1)
static attributeName = Parsernostrum.reg(Grammar.Regex.DotSeparatedSymbols)
static attributeNameQuoted = Parsernostrum.reg(new RegExp('"(' + Grammar.Regex.InsideString.source + ')"'), 1)
static guid = Parsernostrum.reg(new RegExp(`${Grammar.Regex.HexDigit.source}{32}`))
static commaSeparation = Parsernostrum.reg(/\s*,\s*(?!\))/)
static commaOrSpaceSeparation = Parsernostrum.reg(/\s*,\s*(?!\))|\s+/)
static equalSeparation = Parsernostrum.reg(/\s*=\s*/)
static hexColorChannel = Parsernostrum.reg(new RegExp(Grammar.Regex.HexDigit.source + "{2}"))
/* --- Factory --- */
/**
* @template T
* @param {AttributeInfo} attribute
* @param {Parsernostrum} defaultGrammar
* @returns {Parsernostrum}
*/
static grammarFor(attribute, type = attribute?.type, defaultGrammar = this.unknownValue) {
let result = defaultGrammar;
if (type === Array || type instanceof Array) {
if (attribute?.inlined) {
return this.grammarFor(undefined, type[0])
}
result = Parsernostrum.seq(
Parsernostrum.reg(/\(\s*/),
this.grammarFor(undefined, type[0]).sepBy(this.commaSeparation).opt(),
Parsernostrum.reg(/\s*(?:,\s*)?\)/),
).map(([_0, values, _3]) => values instanceof Array ? values : []);
} else if (type instanceof Union) {
result = type.values
.map(v => this.grammarFor(undefined, v))
.reduce((acc, cur) => !cur || cur === this.unknownValue || acc === this.unknownValue
? this.unknownValue
: Parsernostrum.alt(acc, cur)
);
} else if (type instanceof MirroredEntity) {
// @ts-expect-error
return this.grammarFor(undefined, type.getTargetType())
.map(v => new MirroredEntity(type.type, () => v))
} else if (attribute?.constructor === Object) {
result = this.grammarFor(undefined, type);
} else {
switch (type) {
case Boolean:
result = this.boolean;
break
case null:
result = this.null;
break
case Number:
result = this.number;
break
case BigInt:
result = this.bigInt;
break
case String:
result = this.string;
break
default:
if (/** @type {AttributeConstructor} */(type)?.prototype instanceof Serializable) {
result = /** @type {typeof Serializable} */(type).grammar;
}
}
}
if (attribute) {
if (attribute.serialized && type.constructor !== String) {
if (result == this.unknownValue) {
result = this.string;
} else {
result = Parsernostrum.seq(Parsernostrum.str('"'), result, Parsernostrum.str('"')).map(([_0, value, _2]) => value);
}
}
if (attribute.nullable) {
result = Parsernostrum.alt(result, this.null);
}
}
return result
}
/**
* @template {AttributeConstructor} T
* @param {T} entityType
* @param {String[]} key
* @returns {AttributeInfo}
*/
static getAttribute(entityType, key) {
let result;
let type;
if (entityType instanceof Union) {
for (let t of entityType.values) {
if (result = this.getAttribute(t, key)) {
return result
}
}
}
if (entityType instanceof IEntity.constructor) {
// @ts-expect-error
result = entityType.attributes[key[0]];
type = result?.type;
} else if (entityType instanceof Array) {
result = entityType[key[0]];
type = result;
}
if (key.length > 1) {
return this.getAttribute(type, key.slice(1))
}
return result
}
static createAttributeGrammar(
entityType,
attributeName = this.attributeName,
valueSeparator = this.equalSeparation,
handleObjectSet = (obj, k, v) => { }
) {
return Parsernostrum.seq(
attributeName,
valueSeparator,
).chain(([attributeName, _1]) => {
const attributeKey = attributeName.split(Configuration.keysSeparator);
const attributeValue = this.getAttribute(entityType, attributeKey);
return this
.grammarFor(attributeValue)
.map(attributeValue =>
values => {
handleObjectSet(values, attributeKey, attributeValue);
Utility.objectSet(values, attributeKey, attributeValue);
}
)
})
}
/**
* @template {IEntity} T
* @param {(new (...args: any) => T) & EntityConstructor} entityType
* @param {Boolean | Number} acceptUnknownKeys Number to specify the limit or true, to let it be a reasonable value
*/
static createEntityGrammar(entityType, acceptUnknownKeys = true, entriesSeparator = this.commaSeparation) {
const lookbehind = entityType.attributes.lookbehind.default;
return Parsernostrum.seq(
Parsernostrum.reg(
lookbehind instanceof Union
? new RegExp(`(${lookbehind.values.reduce((acc, cur) => acc + "|" + cur)})\\s*\\(\\s*`)
: lookbehind.constructor == String && lookbehind.length > 0
? new RegExp(`(${lookbehind})\\s*\\(\\s*`)
: /()\(\s*/,
1
),
this.createAttributeGrammar(entityType).sepBy(entriesSeparator),
Parsernostrum.reg(/\s*(?:,\s*)?\)/), // trailing comma
)
.map(([lookbehind, attributes, _2]) => {
let values = {};
attributes.forEach(attributeSetter => attributeSetter(values));
if (lookbehind.length) {
values.lookbehind = lookbehind;
}
return values
})
// Decide if we accept the entity or not. It is accepted if it doesn't have too many unexpected keys
.chain(values => {
let totalKeys = Object.keys(values);
// Check missing values
if (
Object.keys(/** @type {AttributeDeclarations} */(entityType.attributes))
.filter(key => entityType.attributes[key].expected)
.find(key => !totalKeys.includes(key) && (key))
) {
return Parsernostrum.failure()
}
const unknownKeys = Object.keys(values).filter(key => !(key in entityType.attributes)).length;
if (!acceptUnknownKeys && unknownKeys > 0) {
return Parsernostrum.failure()
}
return Parsernostrum.success().map(() => new entityType(values))
})
}
/** @type {Parsernostrum} */
static unknownValue // Defined in initializeSerializerFactor to avoid circular include
}
class ColorChannelEntity extends IEntity {
static attributes = {
...super.attributes,
value: AttributeInfo.createValue(0),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.number.map(value => new this(value))
}
constructor(values = 0) {
if (values.constructor !== Object) {
// @ts-expect-error
values = {
value: values,
};
}
super(values);
/** @type {Number} */ this.value;
}
valueOf() {
return this.value
}
toString() {
return this.value.toFixed(6)
}
}
class LinearColorEntity extends IEntity {
static attributes = {
...super.attributes,
R: new AttributeInfo({
type: ColorChannelEntity,
default: () => new ColorChannelEntity(),
expected: true,
}),
G: new AttributeInfo({
type: ColorChannelEntity,
default: () => new ColorChannelEntity(),
expected: true,
}),
B: new AttributeInfo({
type: ColorChannelEntity,
default: () => new ColorChannelEntity(),
expected: true,
}),
A: new AttributeInfo({
type: ColorChannelEntity,
default: () => new ColorChannelEntity(1),
}),
H: new AttributeInfo({
type: ColorChannelEntity,
default: () => new ColorChannelEntity(),
ignored: true,
}),
S: new AttributeInfo({
type: ColorChannelEntity,
default: () => new ColorChannelEntity(),
ignored: true,
}),
V: new AttributeInfo({
type: ColorChannelEntity,
default: () => new ColorChannelEntity(),
ignored: true,
}),
}
static grammar = this.createGrammar()
/** @param {Number} x */
static linearToSRGB(x) {
if (x <= 0) {
return 0
} else if (x >= 1) {
return 1
} else if (x < 0.0031308) {
return x * 12.92
} else {
return Math.pow(x, 1 / 2.4) * 1.055 - 0.055
}
}
/** @param {Number} x */
static sRGBtoLinear(x) {
if (x <= 0) {
return 0
} else if (x >= 1) {
return 1
} else if (x < 0.04045) {
return x / 12.92
} else {
return Math.pow((x + 0.055) / 1.055, 2.4)
}
}
static getWhite() {
return new LinearColorEntity({
R: 1,
G: 1,
B: 1,
})
}
static createGrammar() {
return Grammar.createEntityGrammar(this, false)
}
static getLinearColorFromHexGrammar() {
return Parsernostrum.regArray(new RegExp(
"#(" + Grammar.Regex.HexDigit.source + "{2})"
+ "(" + Grammar.Regex.HexDigit.source + "{2})"
+ "(" + Grammar.Regex.HexDigit.source + "{2})"
+ "(" + Grammar.Regex.HexDigit.source + "{2})?"
)).map(([m, R, G, B, A]) => new this({
R: parseInt(R, 16) / 255,
G: parseInt(G, 16) / 255,
B: parseInt(B, 16) / 255,
A: parseInt(A ?? "FF", 16) / 255,
}))
}
static getLinearColorRGBListGrammar() {
return Parsernostrum.seq(
Parsernostrum.numberByte,
Grammar.commaSeparation,
Parsernostrum.numberByte,
Grammar.commaSeparation,
Parsernostrum.numberByte,
).map(([R, _1, G, _3, B]) => new this({
R: R / 255,
G: G / 255,
B: B / 255,
A: 1,
}))
}
static getLinearColorRGBGrammar() {
return Parsernostrum.seq(
Parsernostrum.reg(/rgb\s*\(\s*/),
this.getLinearColorRGBListGrammar(),
Parsernostrum.reg(/\s*\)/)
).map(([_0, linearColor, _2]) => linearColor)
}
static getLinearColorRGBAGrammar() {
return Parsernostrum.seq(
Parsernostrum.reg(/rgba\s*\(\s*/),
this.getLinearColorRGBListGrammar(),
Parsernostrum.reg(/\s*\)/)
).map(([_0, linearColor, _2]) => linearColor)
}
static getLinearColorFromAnyFormat() {
return Parsernostrum.alt(
this.getLinearColorFromHexGrammar(),
this.getLinearColorRGBAGrammar(),
this.getLinearColorRGBGrammar(),
this.getLinearColorRGBListGrammar(),
)
}
constructor(values) {
if (values instanceof Array) {
values = {
R: values[0] ?? 0,
G: values[1] ?? 0,
B: values[2] ?? 0,
A: values[3] ?? 1,
};
}
super(values);
/** @type {ColorChannelEntity} */ this.R;
/** @type {ColorChannelEntity} */ this.G;
/** @type {ColorChannelEntity} */ this.B;
/** @type {ColorChannelEntity} */ this.A;
/** @type {ColorChannelEntity} */ this.H;
/** @type {ColorChannelEntity} */ this.S;
/** @type {ColorChannelEntity} */ this.V;
this.#updateHSV();
}
#updateHSV() {
const r = this.R.value;
const g = this.G.value;
const b = this.B.value;
if (Utility.approximatelyEqual(r, g) && Utility.approximatelyEqual(r, b) && Utility.approximatelyEqual(g, b)) {
this.S.value = 0;
this.V.value = r;
return
}
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h;
switch (max) {
case min:
h = 0;
break
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break
case g:
h = (b - r) / d + 2;
break
case b:
h = (r - g) / d + 4;
break
}
h /= 6;
this.H.value = h;
this.S.value = max == 0 ? 0 : d / max;
this.V.value = max;
}
/**
* @param {Number} r
* @param {Number} g
* @param {Number} b
* @param {Number} a
*/
setFromRGBA(r, g, b, a = 1) {
this.R.value = r;
this.G.value = g;
this.B.value = b;
this.A.value = a;
this.#updateHSV();
}
/**
* @param {Number} h
* @param {Number} s
* @param {Number} v
* @param {Number} a
*/
setFromHSVA(h, s, v, a = 1) {
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const values = [v, q, p, p, t, v];
const [r, g, b] = [values[i % 6], values[(i + 4) % 6], values[(i + 2) % 6]];
this.R.value = r;
this.G.value = g;
this.B.value = b;
this.A.value = a;
this.H.value = h;
this.S.value = s;
this.V.value = v;
}
/**
* @param {Number} x
* @param {Number} y
* @param {Number} v
* @param {Number} a
*/
setFromWheelLocation(x, y, v, a) {
const [r, theta] = Utility.getPolarCoordinates(x, y, true);
this.setFromHSVA(1 - theta / (2 * Math.PI), r, v, a);
}
toDimmedColor(minV = 0) {
const result = new LinearColorEntity();
result.setFromRGBANumber(this.toNumber());
result.setFromHSVA(
result.H.value,
result.S.value * 0.6,
Math.pow(result.V.value + minV, 0.55) * 0.7
);
return result
}
toCSSRGBValues() {
const r = Math.round(this.R.value * 255);
const g = Math.round(this.G.value * 255);
const b = Math.round(this.B.value * 255);
return i$3`${r}, ${g}, ${b}`
}
toRGBA() {
return [
Math.round(this.R.value * 255),
Math.round(this.G.value * 255),
Math.round(this.B.value * 255),
Math.round(this.A.value * 255),
]
}
toSRGBA() {
return [
Math.round(LinearColorEntity.linearToSRGB(this.R.value) * 255),
Math.round(LinearColorEntity.linearToSRGB(this.G.value) * 255),
Math.round(LinearColorEntity.linearToSRGB(this.B.value) * 255),
Math.round(this.A.value * 255),
]
}
toRGBAString() {
return this
.toRGBA()
.map(v => v.toString(16).toUpperCase().padStart(2, "0"))
.join("")
}
toSRGBAString() {
return this
.toSRGBA()
.map(v => v.toString(16).toUpperCase().padStart(2, "0"))
.join("")
}
toHSVA() {
return [this.H.value, this.S.value, this.V.value, this.A.value]
}
toNumber() {
return (
Math.round(this.R.value * 0xff) << 24)
+ (Math.round(this.G.value * 0xff) << 16)
+ (Math.round(this.B.value * 0xff) << 8)
+ Math.round(this.A.value * 0xff)
}
/** @param {Number} number */
setFromRGBANumber(number) {
this.A.value = (number & 0xff) / 0xff;
this.B.value = ((number >> 8) & 0xff) / 0xff;
this.G.value = ((number >> 16) & 0xff) / 0xff;
this.R.value = ((number >> 24) & 0xff) / 0xff;
this.#updateHSV();
}
/** @param {Number} number */
setFromSRGBANumber(number) {
this.A.value = (number & 0xff) / 0xff;
this.B.value = LinearColorEntity.sRGBtoLinear(((number >> 8) & 0xff) / 0xff);
this.G.value = LinearColorEntity.sRGBtoLinear(((number >> 16) & 0xff) / 0xff);
this.R.value = LinearColorEntity.sRGBtoLinear(((number >> 24) & 0xff) / 0xff);
this.#updateHSV();
}
/** @returns {[Number, Number, Number, Number]} */
toArray() {
return [this.R.value, this.G.value, this.B.value, this.A.value]
}
toString() {
return Utility.printLinearColor(this)
}
}
/** @param {ObjectEntity} entity */
function nodeColor(entity) {
switch (entity.getType()) {
case Configuration.paths.materialExpressionConstant2Vector:
case Configuration.paths.materialExpressionConstant3Vector:
case Configuration.paths.materialExpressionConstant4Vector:
return Configuration.nodeColors.yellow
case Configuration.paths.makeStruct:
return Configuration.nodeColors.darkBlue
case Configuration.paths.materialExpressionMaterialFunctionCall:
return Configuration.nodeColors.blue
case Configuration.paths.materialExpressionFunctionInput:
return Configuration.nodeColors.red
case Configuration.paths.materialExpressionTextureSample:
return Configuration.nodeColors.darkTurquoise
case Configuration.paths.materialExpressionTextureCoordinate:
return Configuration.nodeColors.red
case Configuration.paths.pcgEditorGraphNodeInput:
case Configuration.paths.pcgEditorGraphNodeOutput:
return Configuration.nodeColors.red
}
switch (entity.getClass()) {
case Configuration.paths.callFunction:
return entity.bIsPureFunc
? Configuration.nodeColors.green
: Configuration.nodeColors.blue
case Configuration.paths.niagaraNodeFunctionCall:
return Configuration.nodeColors.darkerBlue
case Configuration.paths.dynamicCast:
return Configuration.nodeColors.turquoise
case Configuration.paths.inputDebugKey:
case Configuration.paths.inputKey:
return Configuration.nodeColors.red
case Configuration.paths.createDelegate:
case Configuration.paths.enumLiteral:
case Configuration.paths.makeArray:
case Configuration.paths.makeMap:
case Configuration.paths.materialGraphNode:
case Configuration.paths.select:
return Configuration.nodeColors.green
case Configuration.paths.executionSequence:
case Configuration.paths.ifThenElse:
case Configuration.paths.macro:
case Configuration.paths.multiGate:
return Configuration.nodeColors.gray
case Configuration.paths.functionEntry:
case Configuration.paths.functionResult:
return Configuration.nodeColors.violet
case Configuration.paths.timeline:
return Configuration.nodeColors.yellow
}
if (entity.switchTarget()) {
return Configuration.nodeColors.lime
}
if (entity.isEvent()) {
return Configuration.nodeColors.red
}
if (entity.isComment()) {
return (entity.CommentColor ? entity.CommentColor : LinearColorEntity.getWhite())
.toDimmedColor()
.toCSSRGBValues()
}
const pcgSubobject = entity.getPcgSubobject();
if (pcgSubobject) {
if (pcgSubobject.NodeTitleColor) {
return pcgSubobject.NodeTitleColor.toDimmedColor(0.1).toCSSRGBValues()
}
switch (entity.PCGNode?.getName(true)) {
case "Branch":
case "Select":
return Configuration.nodeColors.intenseGreen
}
}
if (entity.bIsPureFunc) {
return Configuration.nodeColors.green
}
return Configuration.nodeColors.blue
}
class SVGIcon {
static arrayPin = x`
`
static branchNode = x`
`
static breakStruct = x`
`
static cast = x`
`
static close = x`
`
static convert = x`
`
static correct = x`
`
static delegate = x`
`
static doN = x`
`
static doOnce = x`
`
static enum = x`
`
static event = x`
`
static execPin = x`
`
static expandIcon = x`
`
static flipflop = x`
`
static forEachLoop = x`
`
static functionSymbol = x`
`
static gamepad = x`
`
static genericPin = x`
`
static keyboard = x`
`
static loop = x`
`
static macro = x`
`
static mapPin = x`
`
static makeArray = x`
`
static makeMap = x`
`
static makeSet = x`
`
static makeStruct = x`
`
static metasoundFunction = x`
`
static mouse = x`
`
static node = x`
`
static operationPin = x`
`
static pcgStackPin = x`
`
static pcgPin = x`
`
static pcgParamPin = x`
`
static pcgSpatialPin = x`
`
static plusCircle = x`
`
static questionMark = x`
`
static referencePin = x`
`
static reject = x`
`
static setPin = x`
`
static select = x`
`
static sequence = x`
`
static sound = x`
`
static spawnActor = x`
`
static switch = x`
`
static timer = x`
`
static touchpad = x`
`
}
const sequencerScriptingNameRegex = /\/Script\/SequencerScripting\.MovieSceneScripting(.+)Channel/;
const keyNameValue = {
"A_AccentGrave": "à",
"Add": "Num +",
"C_Cedille": "ç",
"Decimal": "Num .",
"Divide": "Num /",
"E_AccentAigu": "é",
"E_AccentGrave": "è",
"F1": "F1", // Otherwise F and number will be separated
"F10": "F10",
"F11": "F11",
"F12": "F12",
"F2": "F2",
"F3": "F3",
"F4": "F4",
"F5": "F5",
"F6": "F6",
"F7": "F7",
"F8": "F8",
"F9": "F9",
"Gamepad_Special_Left_X": "Touchpad Button X Axis",
"Gamepad_Special_Left_Y": "Touchpad Button Y Axis",
"Mouse2D": "Mouse XY 2D-Axis",
"Multiply": "Num *",
"Section": "§",
"Subtract": "Num -",
"Tilde": "`",
};
function keyName(value) {
/** @type {String} */
let result = keyNameValue[value];
if (result) {
return result
}
result = Utility.numberFromText(value)?.toString();
if (result) {
return result
}
const match = value.match(/NumPad([a-zA-Z]+)/);
if (match) {
result = Utility.numberFromText(match[1]).toString();
if (result) {
return "Num " + result
}
}
}
/** @param {ObjectEntity} entity */
function nodeTitle(entity) {
let input;
switch (entity.getType()) {
case Configuration.paths.asyncAction:
if (entity.ProxyFactoryFunctionName) {
return Utility.formatStringName(entity.ProxyFactoryFunctionName)
}
case Configuration.paths.actorBoundEvent:
case Configuration.paths.componentBoundEvent:
return `${Utility.formatStringName(entity.DelegatePropertyName)} (${entity.ComponentPropertyName ?? "Unknown"})`
case Configuration.paths.callDelegate:
return `Call ${entity.DelegateReference?.MemberName ?? "None"}`
case Configuration.paths.createDelegate:
return "Create Event"
case Configuration.paths.customEvent:
if (entity.CustomFunctionName) {
return entity.CustomFunctionName
}
case Configuration.paths.dynamicCast:
if (!entity.TargetType) {
return "Bad cast node" // Target type not found
}
return `Cast To ${entity.TargetType?.getName()}`
case Configuration.paths.enumLiteral:
return `Literal enum ${entity.Enum?.getName()}`
case Configuration.paths.event:
return `Event ${(entity.EventReference?.MemberName ?? "").replace(/^Receive/, "")}`
case Configuration.paths.executionSequence:
return "Sequence"
case Configuration.paths.forEachElementInEnum:
return `For Each ${entity.Enum?.getName()}`
case Configuration.paths.forEachLoopWithBreak:
return "For Each Loop with Break"
case Configuration.paths.functionEntry:
return entity.FunctionReference?.MemberName === "UserConstructionScript"
? "Construction Script"
: entity.FunctionReference?.MemberName
case Configuration.paths.functionResult:
return "Return Node"
case Configuration.paths.ifThenElse:
return "Branch"
case Configuration.paths.makeStruct:
if (entity.StructType) {
return `Make ${entity.StructType.getName()}`
}
case Configuration.paths.materialExpressionComponentMask: {
const materialObject = entity.getMaterialSubobject();
return `Mask ( ${Configuration.rgba
.filter(k => /** @type {MirroredEntity} */(materialObject[k]).get() === true)
.map(v => v + " ")
.join("")})`
}
case Configuration.paths.materialExpressionConstant:
input ??= [entity.getCustomproperties().find(pinEntity => pinEntity.PinName == "Value")?.DefaultValue];
case Configuration.paths.materialExpressionConstant2Vector:
input ??= [
entity.getCustomproperties().find(pinEntity => pinEntity.PinName == "X")?.DefaultValue,
entity.getCustomproperties().find(pinEntity => pinEntity.PinName == "Y")?.DefaultValue,
];
case Configuration.paths.materialExpressionConstant3Vector:
if (!input) {
/** @type {VectorEntity} */
const vector = entity.getCustomproperties()
.find(pinEntity => pinEntity.PinName == "Constant")
?.DefaultValue;
input = [vector.X, vector.Y, vector.Z];
}
case Configuration.paths.materialExpressionConstant4Vector:
if (!input) {
/** @type {LinearColorEntity} */
const vector = entity.getCustomproperties()
.find(pinEntity => pinEntity.PinName == "Constant")
?.DefaultValue;
input = [vector.R, vector.G, vector.B, vector.A].map(v => v.valueOf());
}
if (input.length > 0) {
return input.map(v => Utility.printExponential(v)).reduce((acc, cur) => acc + "," + cur)
}
break
case Configuration.paths.materialExpressionFunctionInput: {
const materialObject = entity.getMaterialSubobject();
const inputName = materialObject?.InputName ?? "In";
const inputType = materialObject?.InputType?.value.match(/^.+?_(\w+)$/)?.[1] ?? "Vector3";
return `Input ${inputName} (${inputType})`
}
case Configuration.paths.materialExpressionLogarithm:
return "Ln"
case Configuration.paths.materialExpressionLogarithm10:
return "Log10"
case Configuration.paths.materialExpressionLogarithm2:
return "Log2"
case Configuration.paths.materialExpressionMaterialFunctionCall:
const materialFunction = entity.getMaterialSubobject()?.MaterialFunction;
if (materialFunction) {
return materialFunction.getName()
}
break
case Configuration.paths.materialExpressionSquareRoot:
return "Sqrt"
case Configuration.paths.metasoundEditorGraphExternalNode: {
const name = entity["ClassName"]?.["Name"];
if (name) {
switch (name) {
case "Add": return "+"
default: return name
}
}
}
case Configuration.paths.pcgEditorGraphNodeInput:
return "Input"
case Configuration.paths.pcgEditorGraphNodeOutput:
return "Output"
case Configuration.paths.spawnActorFromClass:
let className = entity.getCustomproperties()
.find(pinEntity => pinEntity.PinName == "ReturnValue")
?.PinType
?.PinSubCategoryObject
?.getName();
if (className === "Actor") {
className = null;
}
return `SpawnActor ${Utility.formatStringName(className ?? "NONE")}`
case Configuration.paths.switchEnum:
return `Switch on ${entity.Enum?.getName() ?? "Enum"}`
case Configuration.paths.switchInteger:
return `Switch on Int`
case Configuration.paths.variableGet:
return ""
case Configuration.paths.variableSet:
return "SET"
}
let switchTarget = entity.switchTarget();
if (switchTarget) {
if (switchTarget[0] !== "E") {
switchTarget = Utility.formatStringName(switchTarget);
}
return `Switch on ${switchTarget}`
}
if (entity.isComment()) {
return entity.NodeComment
}
const keyNameSymbol = entity.getHIDAttribute();
if (keyNameSymbol) {
const name = keyNameSymbol.toString();
let title = keyName(name) ?? Utility.formatStringName(name);
if (entity.getClass() === Configuration.paths.inputDebugKey) {
title = "Debug Key " + title;
} else if (entity.getClass() === Configuration.paths.getInputAxisKeyValue) {
title = "Get " + title;
}
return title
}
if (entity.getClass() === Configuration.paths.macro) {
return Utility.formatStringName(entity.MacroGraphReference?.getMacroName())
}
if (entity.isMaterial() && entity.getMaterialSubobject()) {
let result = nodeTitle(entity.getMaterialSubobject());
result = result.match(/Material Expression (.+)/)?.[1] ?? result;
return result
}
if (entity.isPcg() && entity.getPcgSubobject()) {
let pcgSubobject = entity.getPcgSubobject();
let result = pcgSubobject.NodeTitle ? pcgSubobject.NodeTitle : nodeTitle(pcgSubobject);
return result
}
const subgraphObject = entity.getSubgraphObject();
if (subgraphObject) {
return subgraphObject.Graph.getName()
}
const settingsObject = entity.getSettingsObject();
if (settingsObject) {
if (settingsObject.ExportPath.type === Configuration.paths.pcgHiGenGridSizeSettings) {
return `Grid Size: ${(
settingsObject.HiGenGridSize?.toString().match(/\d+/)?.[0]?.concat("00")
?? settingsObject.HiGenGridSize?.toString().match(/^\w+$/)?.[0]
) ?? "256"}`
}
if (settingsObject.BlueprintElementInstance) {
return Utility.formatStringName(settingsObject.BlueprintElementType.getName())
}
if (settingsObject.Operation) {
const match = settingsObject.Name.match(/PCGMetadata(\w+)Settings_\d+/);
if (match) {
return Utility.formatStringName(match[1] + ": " + settingsObject.Operation)
}
}
const settingsSubgraphObject = settingsObject.getSubgraphObject();
if (settingsSubgraphObject && settingsSubgraphObject.Graph) {
return settingsSubgraphObject.Graph.getName()
}
}
let memberName = entity.FunctionReference?.MemberName;
if (memberName) {
const memberParent = entity.FunctionReference.MemberParent?.path ?? "";
switch (memberName) {
case "AddKey":
let result = memberParent.match(sequencerScriptingNameRegex);
if (result) {
return `Add Key (${Utility.formatStringName(result[1])})`
}
case "Concat_StrStr":
return "Append"
}
const memberNameTraceLineMatch = memberName.match(Configuration.lineTracePattern);
if (memberNameTraceLineMatch) {
return "Line Trace"
+ (memberNameTraceLineMatch[1] === "Multi" ? " Multi " : " ")
+ (memberNameTraceLineMatch[2] === ""
? "By Channel"
: Utility.formatStringName(memberNameTraceLineMatch[2])
)
}
switch (memberParent) {
case Configuration.paths.blueprintGameplayTagLibrary:
case Configuration.paths.kismetMathLibrary:
case Configuration.paths.slateBlueprintLibrary:
case Configuration.paths.timeManagementBlueprintLibrary:
const leadingLetter = memberName.match(/[BF]([A-Z]\w+)/);
if (leadingLetter) {
// Some functions start with B or F (Like FCeil, FMax, BMin)
memberName = leadingLetter[1];
}
switch (memberName) {
case "Abs": return "ABS"
case "BooleanAND": return "AND"
case "BooleanNAND": return "NAND"
case "BooleanOR": return "OR"
case "Exp": return "e"
case "LineTraceSingle": return "Line Trace By Channel"
case "Max": return "MAX"
case "MaxInt64": return "MAX"
case "Min": return "MIN"
case "MinInt64": return "MIN"
case "Not_PreBool": return "NOT"
case "Sin": return "SIN"
case "Sqrt": return "SQRT"
case "Square": return "^2"
// Dot products not respecting MemberName pattern
case "CrossProduct2D": return "cross"
case "Vector4_CrossProduct3": return "cross3"
case "DotProduct2D":
case "Vector4_DotProduct":
return "dot"
case "Vector4_DotProduct3": return "dot3"
}
if (memberName.startsWith("Add_")) {
return "+"
}
if (memberName.startsWith("And_")) {
return "&"
}
if (memberName.startsWith("Conv_")) {
return "" // Conversion nodes do not have visible names
}
if (memberName.startsWith("Cross_")) {
return "cross"
}
if (memberName.startsWith("Divide_")) {
return String.fromCharCode(0x00f7)
}
if (memberName.startsWith("Dot_")) {
return "dot"
}
if (memberName.startsWith("EqualEqual_")) {
return "=="
}
if (memberName.startsWith("Greater_")) {
return ">"
}
if (memberName.startsWith("GreaterEqual_")) {
return ">="
}
if (memberName.startsWith("Less_")) {
return "<"
}
if (memberName.startsWith("LessEqual_")) {
return "<="
}
if (memberName.startsWith("Multiply_")) {
return String.fromCharCode(0x2a2f)
}
if (memberName.startsWith("Not_")) {
return "~"
}
if (memberName.startsWith("NotEqual_")) {
return "!="
}
if (memberName.startsWith("Or_")) {
return "|"
}
if (memberName.startsWith("Percent_")) {
return "%"
}
if (memberName.startsWith("Subtract_")) {
return "-"
}
if (memberName.startsWith("Xor_")) {
return "^"
}
break
case Configuration.paths.blueprintSetLibrary:
{
const setOperationMatch = memberName.match(/Set_(\w+)/);
if (setOperationMatch) {
return Utility.formatStringName(setOperationMatch[1]).toUpperCase()
}
}
break
case Configuration.paths.blueprintMapLibrary:
{
const setOperationMatch = memberName.match(/Map_(\w+)/);
if (setOperationMatch) {
return Utility.formatStringName(setOperationMatch[1]).toUpperCase()
}
}
break
case Configuration.paths.kismetArrayLibrary:
{
const arrayOperationMath = memberName.match(/Array_(\w+)/);
if (arrayOperationMath) {
return arrayOperationMath[1].toUpperCase()
}
}
break
}
return Utility.formatStringName(memberName)
}
if (entity.OpName) {
switch (entity.OpName) {
case "Boolean::LogicAnd": return "Logic AND"
case "Boolean::LogicEq": return "=="
case "Boolean::LogicNEq": return "!="
case "Boolean::LogicNot": return "Logic NOT"
case "Boolean::LogicOr": return "Logic OR"
case "Matrix::MatrixMultiply": return "Multiply (Matrix * Matrix)"
case "Matrix::MatrixVectorMultiply": return "Multiply (Matrix * Vector4)"
case "Numeric::Abs": return "Abs"
case "Numeric::Add": return "+"
case "Numeric::DistancePos": return "Distance"
case "Numeric::Mul": return String.fromCharCode(0x2a2f)
}
return Utility.formatStringName(entity.OpName).replaceAll("::", " ")
}
if (entity.FunctionDisplayName) {
return Utility.formatStringName(entity.FunctionDisplayName)
}
if (entity.ObjectRef) {
return entity.ObjectRef.getName()
}
return Utility.formatStringName(entity.getNameAndCounter()[0])
}
/** @param {ObjectEntity} entity */
function nodeIcon(entity) {
if (entity.isMaterial() || entity.isPcg() || entity.isNiagara()) {
return null
}
switch (entity.getType()) {
case Configuration.paths.addDelegate:
case Configuration.paths.asyncAction:
case Configuration.paths.callDelegate:
case Configuration.paths.createDelegate:
case Configuration.paths.functionEntry:
case Configuration.paths.functionResult:
return SVGIcon.node
case Configuration.paths.customEvent: return SVGIcon.event
case Configuration.paths.doN: return SVGIcon.doN
case Configuration.paths.doOnce: return SVGIcon.doOnce
case Configuration.paths.dynamicCast: return SVGIcon.cast
case Configuration.paths.enumLiteral: return SVGIcon.enum
case Configuration.paths.event: return SVGIcon.event
case Configuration.paths.executionSequence:
case Configuration.paths.multiGate:
return SVGIcon.sequence
case Configuration.paths.flipflop:
return SVGIcon.flipflop
case Configuration.paths.forEachElementInEnum:
case Configuration.paths.forLoop:
case Configuration.paths.forLoopWithBreak:
case Configuration.paths.whileLoop:
return SVGIcon.loop
case Configuration.paths.forEachLoop:
case Configuration.paths.forEachLoopWithBreak:
return SVGIcon.forEachLoop
case Configuration.paths.ifThenElse: return SVGIcon.branchNode
case Configuration.paths.isValid: return SVGIcon.questionMark
case Configuration.paths.makeArray: return SVGIcon.makeArray
case Configuration.paths.makeMap: return SVGIcon.makeMap
case Configuration.paths.makeSet: return SVGIcon.makeSet
case Configuration.paths.makeStruct: return SVGIcon.makeStruct
case Configuration.paths.metasoundEditorGraphExternalNode: return SVGIcon.metasoundFunction
case Configuration.paths.select: return SVGIcon.select
case Configuration.paths.spawnActorFromClass: return SVGIcon.spawnActor
case Configuration.paths.timeline: return SVGIcon.timer
}
if (entity.switchTarget()) {
return SVGIcon.switch
}
if (nodeTitle(entity).startsWith("Break")) {
return SVGIcon.breakStruct
}
if (entity.getClass() === Configuration.paths.macro) {
return SVGIcon.macro
}
const hidValue = entity.getHIDAttribute()?.toString();
if (hidValue) {
if (hidValue.includes("Mouse")) {
return SVGIcon.mouse
} else if (hidValue.includes("Gamepad_Special")) {
return SVGIcon.keyboard // It is called Touchpad in UE
} else if (hidValue.includes("Gamepad") || hidValue.includes("Steam")) {
return SVGIcon.gamepad
} else if (hidValue.includes("Touch")) {
return SVGIcon.touchpad
} else {
return SVGIcon.keyboard
}
}
if (entity.getDelegatePin()) {
return SVGIcon.event
}
if (entity.ObjectRef?.type === Configuration.paths.ambientSound) {
return SVGIcon.sound
}
return SVGIcon.functionSymbol
}
var crypto;
if (typeof window === "undefined") {
import('crypto').then(mod => crypto = mod.default).catch();
} else {
crypto = window.crypto;
}
class GuidEntity extends IEntity {
static attributes = {
...super.attributes,
value: AttributeInfo.createValue(""),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.guid.map(v => new this(v))
}
static generateGuid(random = true) {
let values = new Uint32Array(4);
if (random === true) {
crypto.getRandomValues(values);
}
let guid = "";
values.forEach(n => {
guid += ("0".repeat(8) + n.toString(16).toUpperCase()).slice(-8);
});
return new GuidEntity({ value: guid })
}
constructor(values) {
if (!values) {
values = GuidEntity.generateGuid().value;
}
if (values.constructor !== Object) {
values = {
value: values,
};
}
super(values);
/** @type {String} */ this.value;
}
valueOf() {
return this.value
}
toString() {
return this.value
}
}
class ObjectReferenceEntity extends IEntity {
static attributes = {
...super.attributes,
type: new AttributeInfo({
default: "",
serialized: true,
}),
path: new AttributeInfo({
default: "",
serialized: true,
}),
_full: new AttributeInfo({
ignored: true,
}),
}
static quoted = Parsernostrum.regArray(new RegExp(
`'"(${Grammar.Regex.InsideString.source})"'`
+ "|"
+ `'(${Grammar.Regex.InsideSingleQuotedString.source})'`
)).map(([_0, a, b]) => a ?? b)
static path = this.quoted.getParser().parser.regexp.source + "|" + Grammar.Regex.Path.source
static typeReference = Parsernostrum.reg(
new RegExp(Grammar.Regex.Path.source + "|" + Grammar.symbol.getParser().regexp.source)
)
static fullReferenceGrammar = Parsernostrum.regArray(
new RegExp(
"(" + this.typeReference.getParser().regexp.source + ")"
+ "(?:" + this.quoted.getParser().parser.regexp.source + ")"
)
).map(([_full, type, ...path]) => new this({ type, path: path.find(v => v), _full }))
static fullReferenceSerializedGrammar = Parsernostrum.regArray(
new RegExp(
'"(' + Grammar.Regex.InsideString.source + "?)"
+ "(?:'(" + Grammar.Regex.InsideSingleQuotedString.source + `?)')?"`
)
).map(([_full, type, path]) => new this({ type, path, _full }))
static typeReferenceGrammar = this.typeReference.map(v => new this({ type: v, path: "", _full: v }))
static grammar = this.createGrammar()
constructor(values = {}) {
if (values.constructor === String) {
values = {
path: values
};
}
super(values);
if (!values._full || values._full.length === 0) {
this._full = `"${this.type + (this.path ? (`'${this.path}'`) : "")}"`;
}
/** @type {String} */ this.type;
/** @type {String} */ this.path;
}
static createGrammar() {
return Parsernostrum.alt(
this.fullReferenceSerializedGrammar,
this.fullReferenceGrammar,
this.typeReferenceGrammar,
)
}
static createNoneInstance() {
return new ObjectReferenceEntity({ type: "None", path: "" })
}
getName(dropCounter = false) {
return Utility.getNameFromPath(this.path.replace(/_C$/, ""), dropCounter)
}
toString() {
return this._full
}
}
class FunctionReferenceEntity extends IEntity {
static attributes = {
...super.attributes,
MemberParent: AttributeInfo.createType(ObjectReferenceEntity),
MemberName: AttributeInfo.createType(String),
MemberGuid: AttributeInfo.createType(GuidEntity),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(this)
}
constructor(values) {
super(values);
/** @type {ObjectReferenceEntity} */ this.MemberParent;
/** @type {String} */ this.MemberName;
/** @type {GuidEntity} */ this.MemberGuid;
}
}
class IdentifierEntity extends IEntity {
static attributes = {
...super.attributes,
value: AttributeInfo.createValue(""),
}
static attributeConverter = {
fromAttribute: (value, type) => new IdentifierEntity(value),
toAttribute: (value, type) => value.toString()
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.symbol.map(v => new this(v))
}
constructor(values) {
if (values.constructor !== Object) {
values = {
value: values,
};
}
super(values);
/** @type {String} */ this.value;
}
valueOf() {
return this.value
}
toString() {
return this.value
}
}
class IntegerEntity extends IEntity {
static attributes = {
...super.attributes,
value: new AttributeInfo({
default: 0,
predicate: v => v % 1 == 0 && v > 1 << 31 && v < -(1 << 31),
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.numberInteger.map(v => new this(v))
}
/** @param {Number | Object} values */
constructor(values = 0) {
if (values.constructor !== Object) {
values = {
value: values,
};
}
values.value = Math.floor(values.value);
if (values.value === -0) {
values.value = 0;
}
super(values);
/** @type {Number} */ this.value;
}
valueOf() {
return this.value
}
toString() {
return this.value.toString()
}
}
class MacroGraphReferenceEntity extends IEntity {
static attributes = {
...super.attributes,
MacroGraph: new AttributeInfo({
type: ObjectReferenceEntity,
default: () => new ObjectReferenceEntity(),
}),
GraphBlueprint: new AttributeInfo({
type: ObjectReferenceEntity,
default: () => new ObjectReferenceEntity(),
}),
GraphGuid: new AttributeInfo({
type: GuidEntity,
default: () => new GuidEntity(),
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(this)
}
constructor(values) {
super(values);
/** @type {ObjectReferenceEntity} */ this.MacroGraph;
/** @type {ObjectReferenceEntity} */ this.GraphBlueprint;
/** @type {GuidEntity} */ this.GuidEntity;
}
getMacroName() {
const colonIndex = this.MacroGraph.path.search(":");
return this.MacroGraph.path.substring(colonIndex + 1)
}
}
const colors = {
[Configuration.paths.niagaraBool]: i$3`146, 0, 0`,
[Configuration.paths.niagaraDataInterfaceVolumeTexture]: i$3`0, 168, 242`,
[Configuration.paths.niagaraFloat]: i$3`160, 250, 68`,
[Configuration.paths.niagaraMatrix]: i$3`0, 88, 200`,
[Configuration.paths.niagaraNumeric]: i$3`0, 88, 200`,
[Configuration.paths.niagaraPosition]: i$3`251, 146, 251`,
[Configuration.paths.quat4f]: i$3`0, 88, 200`,
[Configuration.paths.rotator]: i$3`157, 177, 251`,
[Configuration.paths.transform]: i$3`227, 103, 0`,
[Configuration.paths.vector]: i$3`251, 198, 34`,
[Configuration.paths.vector3f]: i$3`250, 200, 36`,
[Configuration.paths.vector4f]: i$3`0, 88, 200`,
"Any": i$3`132, 132, 132`,
"Any[]": i$3`132, 132, 132`,
"audio": i$3`252, 148, 252`,
"blue": i$3`0, 0, 255`,
"bool": i$3`146, 0, 0`,
"byte": i$3`0, 109, 99`,
"class": i$3`88, 0, 186`,
"default": i$3`255, 255, 255`,
"delegate": i$3`255, 56, 56`,
"enum": i$3`0, 109, 99`,
"exec": i$3`240, 240, 240`,
"float": i$3`160, 252, 70`,
"green": i$3`0, 255, 0`,
"int": i$3`31, 224, 172`,
"int32": i$3`30, 224, 172`,
"int64": i$3`169, 223, 172`,
"interface": i$3`238, 252, 168`,
"name": i$3`201, 128, 251`,
"object": i$3`0, 168, 242`,
"Param": i$3`255, 166, 39`,
"Param[]": i$3`255, 166, 39`,
"Point": i$3`63, 137, 255`,
"Point[]": i$3`63, 137, 255`,
"real": i$3`54, 208, 0`,
"red": i$3`255, 0, 0`,
"string": i$3`251, 0, 208`,
"struct": i$3`0, 88, 200`,
"Surface": i$3`69, 196, 126`,
"Surface[]": i$3`69, 196, 126`,
"text": i$3`226, 121, 167`,
"time": i$3`148, 252, 252`,
"Volume": i$3`230, 69, 188`,
"Volume[]": i$3`230, 69, 188`,
"wildcard": i$3`128, 120, 120`,
};
const pinColorMaterial = i$3`120, 120, 120`;
/** @param {PinEntity} entity */
function pinColor(entity) {
if (entity.PinType.PinCategory == "mask") {
const result = colors[entity.PinType.PinSubCategory];
if (result) {
return result
}
} else if (entity.PinType.PinCategory == "optional") {
return pinColorMaterial
}
return colors[entity.getType()]
?? colors[entity.PinType.PinCategory.toLowerCase()]
?? colors["default"]
}
/** @param {PinEntity} entity */
function pinTitle(entity) {
let result = entity.PinFriendlyName
? entity.PinFriendlyName.toString()
: Utility.formatStringName(entity.PinName ?? "");
let match;
if (
entity.PinToolTip
// Match up until the first \n excluded or last character
&& (match = entity.PinToolTip.match(/\s*(.+?(?=\n)|.+\S)\s*/))
) {
if (match[1].toLowerCase() === result.toLowerCase()) {
return match[1] // In case they match, then keep the case of the PinToolTip
}
}
return result
}
class ByteEntity extends IntegerEntity {
static attributes = {
...super.attributes,
value: new AttributeInfo({
...super.attributes.value,
predicate: v => v % 1 == 0 && v >= 0 && v < 1 << 8,
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.numberByte.map(v => new this(v))
}
constructor(values = 0) {
super(values);
}
}
class SymbolEntity extends IEntity {
static attributes = {
...super.attributes,
value: AttributeInfo.createValue(""),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.symbol.map(v => new this(v))
}
/** @param {String | Object} values */
constructor(values) {
if (values.constructor !== Object) {
values = {
value: values,
};
}
super(values);
/** @type {String} */ this.value;
}
valueOf() {
return this.value
}
toString() {
return this.value
}
}
class EnumEntity extends SymbolEntity {
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.symbol.map(v => new this(v))
}
}
class EnumDisplayValueEntity extends EnumEntity {
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.reg(Grammar.Regex.InsideString).map(v => new this(v))
}
}
class InvariantTextEntity extends IEntity {
static attributes = {
...super.attributes,
value: AttributeInfo.createValue(""),
lookbehind: new AttributeInfo({
...super.attributes.lookbehind,
default: "INVTEXT",
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.seq(
Parsernostrum.reg(new RegExp(`${this.attributes.lookbehind.default}\\s*\\(`)),
Grammar.grammarFor(this.attributes.value),
Parsernostrum.reg(/\s*\)/)
)
.map(([_0, value, _2]) => value),
Parsernostrum.reg(new RegExp(this.attributes.lookbehind.default)) // InvariantTextEntity can not have arguments
.map(() => "")
).map(value => new this(value))
}
constructor(values) {
if (values.constructor !== Object) {
values = {
value: values,
};
}
super(values);
/** @type {String} */ this.value;
}
toString() {
return this.value
}
}
class LocalizedTextEntity extends IEntity {
static attributes = {
...super.attributes,
namespace: AttributeInfo.createValue(""),
key: AttributeInfo.createValue(""),
value: AttributeInfo.createValue(""),
lookbehind: new AttributeInfo({
...super.attributes.lookbehind,
default: "NSLOCTEXT",
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.regArray(new RegExp(
String.raw`${this.attributes.lookbehind.default}\s*\(`
+ String.raw`\s*"(${Grammar.Regex.InsideString.source})"\s*,`
+ String.raw`\s*"(${Grammar.Regex.InsideString.source})"\s*,`
+ String.raw`\s*"(${Grammar.Regex.InsideString.source})"\s*`
+ String.raw`(?:,\s+)?`
+ String.raw`\)`,
"m"
)).map(matchResult => new this({
namespace: Utility.unescapeString(matchResult[1]),
key: Utility.unescapeString(matchResult[2]),
value: Utility.unescapeString(matchResult[3]),
}))
}
constructor(values) {
super(values);
/** @type {String} */ this.namespace;
/** @type {String} */ this.key;
/** @type {String} */ this.value;
}
toString() {
return Utility.capitalFirstLetter(this.value)
}
}
class FormatTextEntity extends IEntity {
static attributes = {
...super.attributes,
value: new AttributeInfo({
type: [new Union(String, LocalizedTextEntity, InvariantTextEntity, FormatTextEntity)],
default: [],
}),
lookbehind: /** @type {AttributeInfo>} */(new AttributeInfo({
...super.attributes.lookbehind,
default: new Union("LOCGEN_FORMAT_NAMED", "LOCGEN_FORMAT_ORDERED"),
})),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.seq(
Parsernostrum.reg(
// Resulting regex: /(LOCGEN_FORMAT_NAMED|LOCGEN_FORMAT_ORDERED)\s*/
new RegExp(`(${this.attributes.lookbehind.default.values.reduce((acc, cur) => acc + "|" + cur)})\\s*`),
1
),
Grammar.grammarFor(this.attributes.value)
)
.map(([lookbehind, values]) => {
const result = new this({
value: values,
lookbehind,
});
return result
})
}
constructor(values) {
super(values);
/** @type {(String | LocalizedTextEntity | InvariantTextEntity | FormatTextEntity)[]} */ this.value;
}
toString() {
const pattern = this.value?.[0]?.toString(); // The pattern is always the first element of the array
if (!pattern) {
return ""
}
const values = this.value.slice(1).map(v => v.toString());
return this.lookbehind == "LOCGEN_FORMAT_NAMED"
? pattern.replaceAll(/\{([a-zA-Z]\w*)\}/g, (substring, arg) => {
const argLocation = values.indexOf(arg) + 1;
return argLocation > 0 && argLocation < values.length
? values[argLocation]
: substring
})
: this.lookbehind == "LOCGEN_FORMAT_ORDERED"
? pattern.replaceAll(/\{(\d+)\}/g, (substring, arg) => {
const argValue = Number(arg);
return argValue < values.length
? values[argValue]
: substring
})
: ""
}
}
class Integer64Entity extends IEntity {
static attributes = {
...super.attributes,
value: new AttributeInfo({
default: 0n,
predicate: v => v >= -(1n << 63n) && v < 1n << 63n,
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.numberBigInteger.map(v => new this(v))
}
/** @param {BigInt | Number | Object} values */
constructor(values = 0) {
if (values.constructor !== Object) {
values = {
value: values,
};
}
if (values.value === -0) {
values.value = 0n;
}
super(values);
/** @type {BigInt} */ this.value;
}
valueOf() {
return this.value
}
toString() {
return this.value.toString()
}
}
class PathSymbolEntity extends IEntity {
static attributes = {
...super.attributes,
value: new AttributeInfo({
default: "",
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.symbol.map(v => new this(v))
}
constructor(values) {
if (values.constructor !== Object) {
values = {
value: values,
};
}
super(values);
/** @type {String} */ this.value;
}
valueOf() {
return this.value
}
toString() {
return this.value
}
}
class PinReferenceEntity extends IEntity {
static attributes = {
...super.attributes,
objectName: AttributeInfo.createType(PathSymbolEntity),
pinGuid: AttributeInfo.createType(GuidEntity),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.seq(
PathSymbolEntity.grammar,
Parsernostrum.whitespace,
GuidEntity.grammar
).map(
([objectName, _1, pinGuid]) => new this({
objectName: objectName,
pinGuid: pinGuid,
})
)
}
constructor(values) {
super(values);
/** @type {PathSymbolEntity} */ this.objectName;
/** @type {GuidEntity} */ this.pinGuid;
}
}
class PinTypeEntity extends IEntity {
static attributes = {
...super.attributes,
PinCategory: AttributeInfo.createValue(""),
PinSubCategory: AttributeInfo.createValue(""),
PinSubCategoryObject: new AttributeInfo({
type: ObjectReferenceEntity,
default: () => ObjectReferenceEntity.createNoneInstance(),
}),
PinSubCategoryMemberReference: new AttributeInfo({
type: FunctionReferenceEntity,
default: null,
}),
PinValueType: new AttributeInfo({
type: PinTypeEntity,
default: null,
}),
ContainerType: AttributeInfo.createType(PathSymbolEntity),
bIsReference: AttributeInfo.createValue(false),
bIsConst: AttributeInfo.createValue(false),
bIsWeakPointer: AttributeInfo.createValue(false),
bIsUObjectWrapper: AttributeInfo.createValue(false),
bSerializeAsSinglePrecisionFloat: AttributeInfo.createValue(false),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(this)
}
constructor(values = {}, suppressWarns = false) {
super(values, suppressWarns);
/** @type {String} */ this.PinCategory;
/** @type {String} */ this.PinSubCategory;
/** @type {ObjectReferenceEntity} */ this.PinSubCategoryObject;
/** @type {FunctionReferenceEntity} */ this.PinSubCategoryMemberReference;
/** @type {PinTypeEntity} */ this.PinValueType;
/** @type {PathSymbolEntity} */ this.ContainerType;
/** @type {Boolean} */ this.bIsReference;
/** @type {Boolean} */ this.bIsConst;
/** @type {Boolean} */ this.bIsWeakPointer;
/** @type {Boolean} */ this.bIsUObjectWrapper;
/** @type {Boolean} */ this.bIsUObjectWrapper;
/** @type {Boolean} */ this.bSerializeAsSinglePrecisionFloat;
}
/** @param {PinTypeEntity} other */
copyTypeFrom(other) {
this.PinCategory = other.PinCategory;
this.PinSubCategory = other.PinSubCategory;
this.PinSubCategoryObject = other.PinSubCategoryObject;
this.PinSubCategoryMemberReference = other.PinSubCategoryMemberReference;
this.PinValueType = other.PinValueType;
this.ContainerType = other.ContainerType;
this.bIsReference = other.bIsReference;
this.bIsConst = other.bIsConst;
this.bIsWeakPointer = other.bIsWeakPointer;
this.bIsUObjectWrapper = other.bIsUObjectWrapper;
this.bSerializeAsSinglePrecisionFloat = other.bSerializeAsSinglePrecisionFloat;
}
}
class Vector2DEntity extends IEntity {
static attributes = {
...super.attributes,
X: new AttributeInfo({
default: 0,
expected: true,
}),
Y: new AttributeInfo({
default: 0,
expected: true,
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(this, false)
}
constructor(values) {
super(values);
/** @type {Number} */ this.X;
/** @type {Number} */ this.Y;
}
/** @returns {[Number, Number]} */
toArray() {
return [this.X, this.Y]
}
}
class RBSerializationVector2DEntity extends Vector2DEntity {
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
/X\s*=\s*/.source + "(?" + Parsernostrum.number.getParser().parser.regexp.source + ")"
+ "\\s+"
+ /Y\s*=\s*/.source + "(?" + Parsernostrum.number.getParser().parser.regexp.source + ")"
)).map(({ groups: { x, y } }) => new this({
X: Number(x),
Y: Number(y),
})),
Vector2DEntity.grammar
)
}
}
class RotatorEntity extends IEntity {
static attributes = {
...super.attributes,
R: new AttributeInfo({
default: 0,
expected: true,
}),
P: new AttributeInfo({
default: 0,
expected: true,
}),
Y: new AttributeInfo({
default: 0,
expected: true,
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(this, false)
}
constructor(values) {
super(values);
/** @type {Number} */ this.R;
/** @type {Number} */ this.P;
/** @type {Number} */ this.Y;
}
getRoll() {
return this.R
}
getPitch() {
return this.P
}
getYaw() {
return this.Y
}
}
class SimpleSerializationRotatorEntity extends RotatorEntity {
static grammar = this.createGrammar()
static createGrammar() {
const number = Parsernostrum.number.getParser().parser.regexp.source;
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
"(" + number + ")"
+ "\\s*,\\s*"
+ "(" + number + ")"
+ "\\s*,\\s*"
+ "(" + number + ")"
)).map(([_, p, y, r]) => new this({
R: Number(r),
P: Number(p),
Y: Number(y),
})),
RotatorEntity.grammar
)
}
}
class SimpleSerializationVector2DEntity extends Vector2DEntity {
static grammar = this.createGrammar()
static createGrammar() {
const number = Parsernostrum.number.getParser().parser.regexp.source;
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
"(" + number + ")"
+ "\\s*,\\s*"
+ "(" + number + ")"
)).map(([_, x, y]) => new this({
X: Number(x),
Y: Number(y),
})),
Vector2DEntity.grammar
)
}
}
class Vector4DEntity extends IEntity {
static attributes = {
...super.attributes,
X: new AttributeInfo({
default: 0,
expected: true,
}),
Y: new AttributeInfo({
default: 0,
expected: true,
}),
Z: new AttributeInfo({
default: 0,
expected: true,
}),
W: new AttributeInfo({
default: 0,
expected: true,
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(Vector4DEntity, false)
}
constructor(values) {
super(values);
/** @type {Number} */ this.X;
/** @type {Number} */ this.Y;
/** @type {Number} */ this.Z;
/** @type {Number} */ this.W;
}
/** @returns {[Number, Number, Number, Number]} */
toArray() {
return [this.X, this.Y, this.Z, this.W]
}
}
class SimpleSerializationVector4DEntity extends Vector4DEntity {
static grammar = this.createGrammar()
static createGrammar() {
const number = Parsernostrum.number.getParser().parser.regexp.source;
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
"(" + number + ")"
+ "\\s*,\\s*"
+ "(" + number + ")"
+ "\\s*,\\s*"
+ "(" + number + ")"
+ "\\s*,\\s*"
+ "(" + number + ")"
))
.map(([_0, x, y, z, w]) => new this({
X: Number(x),
Y: Number(y),
Z: Number(z),
W: Number(w),
})),
Vector4DEntity.grammar
)
}
}
class VectorEntity extends IEntity {
static attributes = {
...super.attributes,
X: new AttributeInfo({
default: 0,
expected: true,
}),
Y: new AttributeInfo({
default: 0,
expected: true,
}),
Z: new AttributeInfo({
default: 0,
expected: true,
}),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(VectorEntity, false)
}
constructor(values) {
super(values);
/** @type {Number} */ this.X;
/** @type {Number} */ this.Y;
/** @type {Number} */ this.Z;
}
/** @returns {[Number, Number, Number]} */
toArray() {
return [this.X, this.Y, this.Z]
}
}
class SimpleSerializationVectorEntity extends VectorEntity {
static grammar = this.createGrammar()
static createGrammar() {
const number = Parsernostrum.number.getParser().parser.regexp.source;
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
"(" + number + ")"
+ "\\s*,\\s*"
+ "(" + number + ")"
+ "\\s*,\\s*"
+ "(" + number + ")"
))
.map(([_0, x, y, z]) => new this({
X: Number(x),
Y: Number(y),
Z: Number(z),
})),
VectorEntity.grammar
)
}
}
/** @template {TerminalAttribute} T */
class PinEntity extends IEntity {
static #typeEntityMap = {
[Configuration.paths.linearColor]: LinearColorEntity,
[Configuration.paths.rotator]: RotatorEntity,
[Configuration.paths.vector]: VectorEntity,
[Configuration.paths.vector2D]: Vector2DEntity,
[Configuration.paths.vector4f]: Vector4DEntity,
"bool": Boolean,
"byte": ByteEntity,
"enum": EnumEntity,
"exec": String,
"int": IntegerEntity,
"int64": Integer64Entity,
"name": String,
"real": Number,
"string": String,
}
static #alternativeTypeEntityMap = {
"enum": EnumDisplayValueEntity,
"rg": RBSerializationVector2DEntity,
[Configuration.paths.rotator]: SimpleSerializationRotatorEntity,
[Configuration.paths.vector]: SimpleSerializationVectorEntity,
[Configuration.paths.vector2D]: SimpleSerializationVector2DEntity,
[Configuration.paths.vector3f]: SimpleSerializationVectorEntity,
[Configuration.paths.vector4f]: SimpleSerializationVector4DEntity,
}
static attributes = {
...super.attributes,
lookbehind: new AttributeInfo({
default: "Pin",
ignored: true,
}),
objectEntity: new AttributeInfo({
ignored: true,
}),
pinIndex: new AttributeInfo({
type: Number,
ignored: true,
}),
PinId: new AttributeInfo({
type: GuidEntity,
default: () => new GuidEntity()
}),
PinName: AttributeInfo.createValue(""),
PinFriendlyName: AttributeInfo.createType(new Union(LocalizedTextEntity, FormatTextEntity, InvariantTextEntity, String)),
PinToolTip: AttributeInfo.createType(String),
Direction: AttributeInfo.createType(String),
PinType: new AttributeInfo({
type: PinTypeEntity,
default: () => new PinTypeEntity(),
inlined: true,
}),
LinkedTo: AttributeInfo.createType([PinReferenceEntity]),
SubPins: AttributeInfo.createType([PinReferenceEntity]),
ParentPin: AttributeInfo.createType(PinReferenceEntity),
DefaultValue: new AttributeInfo({
type: new ComputedType(
/** @param {PinEntity} pinEntity */
pinEntity => pinEntity.getEntityType(true) ?? String
),
serialized: true,
}),
AutogeneratedDefaultValue: AttributeInfo.createType(String),
DefaultObject: AttributeInfo.createType(ObjectReferenceEntity),
PersistentGuid: AttributeInfo.createType(GuidEntity),
bHidden: AttributeInfo.createValue(false),
bNotConnectable: AttributeInfo.createValue(false),
bDefaultValueIsReadOnly: AttributeInfo.createValue(false),
bDefaultValueIsIgnored: AttributeInfo.createValue(false),
bAdvancedView: AttributeInfo.createValue(false),
bOrphanedPin: AttributeInfo.createValue(false),
}
static grammar = this.createGrammar()
#recomputesNodeTitleOnChange = false
set recomputesNodeTitleOnChange(value) {
this.#recomputesNodeTitleOnChange = value;
}
get recomputesNodeTitleOnChange() {
return this.#recomputesNodeTitleOnChange
}
static createGrammar() {
return Grammar.createEntityGrammar(this)
}
constructor(values = {}, suppressWarns = false) {
super(values, suppressWarns);
/** @type {ObjectEntity} */ this.objectEntity;
/** @type {Number} */ this.pinIndex;
/** @type {GuidEntity} */ this.PinId;
/** @type {String} */ this.PinName;
/** @type {LocalizedTextEntity | String} */ this.PinFriendlyName;
/** @type {String} */ this.PinToolTip;
/** @type {String} */ this.Direction;
/** @type {PinTypeEntity} */ this.PinType;
/** @type {PinReferenceEntity[]} */ this.LinkedTo;
/** @type {T} */ this.DefaultValue;
/** @type {String} */ this.AutogeneratedDefaultValue;
/** @type {ObjectReferenceEntity} */ this.DefaultObject;
/** @type {GuidEntity} */ this.PersistentGuid;
/** @type {Boolean} */ this.bHidden;
/** @type {Boolean} */ this.bNotConnectable;
/** @type {Boolean} */ this.bDefaultValueIsReadOnly;
/** @type {Boolean} */ this.bDefaultValueIsIgnored;
/** @type {Boolean} */ this.bAdvancedView;
/** @type {Boolean} */ this.bOrphanedPin;
}
/** @param {ObjectEntity} objectEntity */
static fromLegacyObject(objectEntity) {
return new PinEntity(objectEntity, true)
}
getType() {
const category = this.PinType.PinCategory.toLocaleLowerCase();
if (category === "struct" || category === "class" || category === "object" || category === "type") {
return this.PinType.PinSubCategoryObject.path
}
if (this.isEnum()) {
return "enum"
}
if (this.objectEntity?.isPcg()) {
const pcgSuboject = this.objectEntity.getPcgSubobject();
const pinObjectReference = this.isInput()
? pcgSuboject.InputPins?.[this.pinIndex]
: pcgSuboject.OutputPins?.[this.pinIndex];
if (pinObjectReference) {
/** @type {ObjectEntity} */
const pinObject = pcgSuboject[Configuration.subObjectAttributeNameFromReference(pinObjectReference, true)];
let allowedTypes = pinObject.Properties?.AllowedTypes?.toString() ?? "";
if (allowedTypes == "") {
allowedTypes = this.PinType.PinCategory ?? "";
if (allowedTypes == "") {
allowedTypes = "Any";
}
}
if (allowedTypes) {
if (
pinObject.Properties.bAllowMultipleData !== false
&& pinObject.Properties.bAllowMultipleConnections !== false
) {
allowedTypes += "[]";
}
return allowedTypes
}
}
}
if (category === "optional") {
switch (this.PinType.PinSubCategory) {
case "red":
return "real"
case "rg":
return "rg"
case "rgb":
return Configuration.paths.vector
case "rgba":
return Configuration.paths.linearColor
default:
return this.PinType.PinSubCategory
}
}
return category
}
getEntityType(alternative = false) {
const typeString = this.getType();
const entity = PinEntity.#typeEntityMap[typeString];
const alternativeEntity = PinEntity.#alternativeTypeEntityMap[typeString];
return alternative && alternativeEntity !== undefined
? alternativeEntity
: entity
}
pinTitle() {
return pinTitle(this)
}
/** @param {PinEntity} other */
copyTypeFrom(other) {
this.PinType.PinCategory = other.PinType.PinCategory;
this.PinType.PinSubCategory = other.PinType.PinSubCategory;
this.PinType.PinSubCategoryObject = other.PinType.PinSubCategoryObject;
this.PinType.PinSubCategoryMemberReference = other.PinType.PinSubCategoryMemberReference;
this.PinType.PinValueType = other.PinType.PinValueType;
this.PinType.ContainerType = other.PinType.ContainerType;
this.PinType.bIsReference = other.PinType.bIsReference;
this.PinType.bIsConst = other.PinType.bIsConst;
this.PinType.bIsWeakPointer = other.PinType.bIsWeakPointer;
this.PinType.bIsUObjectWrapper = other.PinType.bIsUObjectWrapper;
this.PinType.bSerializeAsSinglePrecisionFloat = other.PinType.bSerializeAsSinglePrecisionFloat;
}
getDefaultValue(maybeCreate = false) {
if (this.DefaultValue === undefined && maybeCreate) {
// @ts-expect-error
this.DefaultValue = new (this.getEntityType(true))();
}
return this.DefaultValue
}
isEnum() {
const type = this.PinType.PinSubCategoryObject.type;
return type === Configuration.paths.enum
|| type === Configuration.paths.userDefinedEnum
|| type.toLowerCase() === "enum"
}
isExecution() {
return this.PinType.PinCategory === "exec"
}
isHidden() {
return this.bHidden
}
isInput() {
return !this.bHidden && this.Direction != "EGPD_Output"
}
isOutput() {
return !this.bHidden && this.Direction == "EGPD_Output"
}
isLinked() {
return this.LinkedTo?.length > 0 ?? false
}
/**
* @param {String} targetObjectName
* @param {PinEntity} targetPinEntity
* @returns true if it was not already linked to the tarket
*/
linkTo(targetObjectName, targetPinEntity) {
const linkFound = this.LinkedTo?.some(pinReferenceEntity =>
pinReferenceEntity.objectName.toString() == targetObjectName
&& pinReferenceEntity.pinGuid.valueOf() == targetPinEntity.PinId.valueOf()
);
if (!linkFound) {
(this.LinkedTo ??= []).push(new PinReferenceEntity({
objectName: targetObjectName,
pinGuid: targetPinEntity.PinId,
}));
return true
}
return false // Already linked
}
/**
* @param {String} targetObjectName
* @param {PinEntity} targetPinEntity
* @returns true if it was linked to the target
*/
unlinkFrom(targetObjectName, targetPinEntity) {
const indexElement = this.LinkedTo?.findIndex(pinReferenceEntity => {
return pinReferenceEntity.objectName.toString() == targetObjectName
&& pinReferenceEntity.pinGuid.valueOf() == targetPinEntity.PinId.valueOf()
});
if (indexElement >= 0) {
this.LinkedTo.splice(indexElement, 1);
if (this.LinkedTo.length === 0 && PinEntity.attributes.LinkedTo.default === undefined) {
this.LinkedTo = undefined;
}
return true
}
return false
}
getSubCategory() {
return this.PinType.PinSubCategoryObject.path
}
pinColor() {
return pinColor(this)
}
}
class ScriptVariableEntity extends IEntity {
static attributes = {
...super.attributes,
ScriptVariable: AttributeInfo.createType(ObjectReferenceEntity),
OriginalChangeId: AttributeInfo.createType(GuidEntity),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(this)
}
constructor(values = {}, suppressWarns = false) {
super(values, suppressWarns);
/** @type {ObjectReferenceEntity} */ this.ScriptVariable;
/** @type {GuidEntity} */ this.OriginalChangeId;
}
}
class UnknownPinEntity extends PinEntity {
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.seq(
Parsernostrum.reg(
new RegExp(`(${Grammar.Regex.Symbol.source})\\s*\\(\\s*`),
1
),
Grammar.createAttributeGrammar(this).sepBy(Grammar.commaSeparation),
Parsernostrum.reg(/\s*(?:,\s*)?\)/)
).map(([lookbehind, attributes, _2]) => {
lookbehind ??= "";
let values = {};
if (lookbehind.length) {
values.lookbehind = lookbehind;
}
attributes.forEach(attributeSetter => attributeSetter(values));
return new this(values)
})
}
constructor(values = {}) {
super(values, true);
}
}
class VariableReferenceEntity extends IEntity {
static attributes = {
...super.attributes,
MemberScope: AttributeInfo.createType(String),
MemberName: AttributeInfo.createValue(""),
MemberGuid: AttributeInfo.createType(GuidEntity),
bSelfContext: AttributeInfo.createType(Boolean),
}
static grammar = this.createGrammar()
static createGrammar() {
return Grammar.createEntityGrammar(this)
}
constructor(values) {
super(values);
/** @type {String} */ this.MemberName;
/** @type {GuidEntity} */ this.GuidEntity;
/** @type {Boolean} */ this.bSelfContext;
}
}
/** @param {PinEntity} pinEntity */
const indexFromUpperCaseLetterName = pinEntity =>
pinEntity.PinName.match(/^\s*([A-Z])\s*$/)?.[1]?.charCodeAt(0) - "A".charCodeAt(0);
/** @param {ObjectEntity} entity */
function nodeVariadic(entity) {
/** @type {() => PinEntity[]} */
let pinEntities;
/** @type {(pinEntity: PinEntity) => Number} */
let pinIndexFromEntity;
/** @type {(newPinIndex: Number, minIndex: Number, maxIndex: Number, newPin: PinEntity) => String} */
let pinNameFromIndex;
const type = entity.getType();
let name;
switch (type) {
case Configuration.paths.commutativeAssociativeBinaryOperator:
case Configuration.paths.promotableOperator:
name = entity.FunctionReference?.MemberName;
switch (name) {
default:
if (
!name?.startsWith("Add_")
&& !name?.startsWith("Subtract_")
&& !name?.startsWith("Multiply_")
&& !name?.startsWith("Divide_")
) {
break
}
case "And_Int64Int64":
case "And_IntInt":
case "BMax":
case "BMin":
case "BooleanAND":
case "BooleanNAND":
case "BooleanOR":
case "Concat_StrStr":
case "FMax":
case "FMin":
case "Max":
case "MaxInt64":
case "Min":
case "MinInt64":
case "Or_Int64Int64":
case "Or_IntInt":
pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isInput());
pinIndexFromEntity ??= indexFromUpperCaseLetterName;
pinNameFromIndex ??= (index, min = -1, max = -1) => {
const result = String.fromCharCode(index >= 0 ? index : max + "A".charCodeAt(0) + 1);
entity.NumAdditionalInputs = pinEntities().length - 1;
return result
};
break
}
break
case Configuration.paths.multiGate:
pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.match(/^\s*Out[_\s]+(\d+)\s*$/i)?.[1]);
pinNameFromIndex ??= (index, min = -1, max = -1, newPin) =>
`Out ${index >= 0 ? index : min > 0 ? "Out 0" : max + 1}`;
break
// case Configuration.paths.niagaraNodeOp:
// pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isInput())
// pinIndexFromEntity ??= indexFromUpperCaseLetterName
// pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => {
// const result = String.fromCharCode(index >= 0 ? index : max + "A".charCodeAt(0) + 1)
// entity.AddedPins ??= []
// entity.AddedPins.push(newPin)
// return result
// }
// break
case Configuration.paths.switchInteger:
pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.match(/^\s*(\d+)\s*$/)?.[1]);
pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => (index < 0 ? max + 1 : index).toString();
break
case Configuration.paths.switchGameplayTag:
pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => {
const result = `Case_${index >= 0 ? index : min > 0 ? "0" : max + 1}`;
entity.PinNames ??= [];
entity.PinNames.push(result);
delete entity.PinTags[entity.PinTags.length - 1];
entity.PinTags[entity.PinTags.length] = null;
return result
};
case Configuration.paths.switchName:
case Configuration.paths.switchString:
pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.match(/^\s*Case[_\s]+(\d+)\s*$/i)?.[1]);
pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => {
const result = `Case_${index >= 0 ? index : min > 0 ? "0" : max + 1}`;
entity.PinNames ??= [];
entity.PinNames.push(result);
return result
};
break
}
if (pinEntities) {
return () => {
let min = Number.MAX_SAFE_INTEGER;
let max = Number.MIN_SAFE_INTEGER;
let values = [];
const modelPin = pinEntities().reduce(
(acc, cur) => {
const value = pinIndexFromEntity(cur);
if (!isNaN(value)) {
values.push(value);
min = Math.min(value, min);
if (value > max) {
max = value;
return cur
}
} else if (acc === undefined) {
return cur
}
return acc
},
undefined
);
if (min === Number.MAX_SAFE_INTEGER || max === Number.MIN_SAFE_INTEGER) {
min = undefined;
max = undefined;
}
if (!modelPin) {
return null
}
values.sort((a, b) => a < b ? -1 : a === b ? 0 : 1);
let prev = values[0];
let index = values.findIndex(
// Search for a gap
value => {
const result = value - prev > 1;
prev = value;
return result
}
);
const newPin = new PinEntity(modelPin);
newPin.PinId = GuidEntity.generateGuid();
newPin.PinName = pinNameFromIndex(index, min, max, newPin);
newPin.PinToolTip = undefined;
entity.getCustomproperties(true).push(newPin);
return newPin
}
}
}
class ObjectEntity extends IEntity {
static attributes = {
...super.attributes,
isExported: new AttributeInfo({
type: Boolean,
ignored: true,
}),
Class: AttributeInfo.createType(ObjectReferenceEntity),
Name: AttributeInfo.createType(String),
Archetype: AttributeInfo.createType(ObjectReferenceEntity),
ExportPath: AttributeInfo.createType(ObjectReferenceEntity),
ObjectRef: AttributeInfo.createType(ObjectReferenceEntity),
BlueprintElementType: AttributeInfo.createType(ObjectReferenceEntity),
BlueprintElementInstance: AttributeInfo.createType(ObjectReferenceEntity),
PinTags: new AttributeInfo({
type: [null],
inlined: true,
}),
PinNames: new AttributeInfo({
type: [String],
inlined: true,
}),
AxisKey: AttributeInfo.createType(SymbolEntity),
InputAxisKey: AttributeInfo.createType(SymbolEntity),
InputName: AttributeInfo.createType(String),
InputType: AttributeInfo.createType(SymbolEntity),
NumAdditionalInputs: AttributeInfo.createType(Number),
bIsPureFunc: AttributeInfo.createType(Boolean),
bIsConstFunc: AttributeInfo.createType(Boolean),
bIsCaseSensitive: AttributeInfo.createType(Boolean),
VariableReference: AttributeInfo.createType(VariableReferenceEntity),
SelfContextInfo: AttributeInfo.createType(SymbolEntity),
DelegatePropertyName: AttributeInfo.createType(String),
DelegateOwnerClass: AttributeInfo.createType(ObjectReferenceEntity),
ComponentPropertyName: AttributeInfo.createType(String),
EventReference: AttributeInfo.createType(FunctionReferenceEntity),
FunctionReference: AttributeInfo.createType(FunctionReferenceEntity),
FunctionScript: AttributeInfo.createType(ObjectReferenceEntity),
CustomFunctionName: AttributeInfo.createType(String),
TargetType: AttributeInfo.createType(ObjectReferenceEntity),
MacroGraphReference: AttributeInfo.createType(MacroGraphReferenceEntity),
Enum: AttributeInfo.createType(ObjectReferenceEntity),
EnumEntries: new AttributeInfo({
type: [String],
inlined: true,
}),
InputKey: AttributeInfo.createType(SymbolEntity),
OpName: AttributeInfo.createType(String),
CachedChangeId: AttributeInfo.createType(GuidEntity),
FunctionDisplayName: AttributeInfo.createType(String),
AddedPins: new AttributeInfo({
type: [UnknownPinEntity],
default: () => [],
inlined: true,
silent: true,
}),
ChangeId: AttributeInfo.createType(GuidEntity),
MaterialFunction: AttributeInfo.createType(ObjectReferenceEntity),
bOverrideFunction: AttributeInfo.createType(Boolean),
bInternalEvent: AttributeInfo.createType(Boolean),
bConsumeInput: AttributeInfo.createType(Boolean),
bExecuteWhenPaused: AttributeInfo.createType(Boolean),
bOverrideParentBinding: AttributeInfo.createType(Boolean),
bControl: AttributeInfo.createType(Boolean),
bAlt: AttributeInfo.createType(Boolean),
bShift: AttributeInfo.createType(Boolean),
bCommand: AttributeInfo.createType(Boolean),
CommentColor: AttributeInfo.createType(LinearColorEntity),
bCommentBubbleVisible_InDetailsPanel: AttributeInfo.createType(Boolean),
bColorCommentBubble: AttributeInfo.createType(Boolean),
ProxyFactoryFunctionName: AttributeInfo.createType(String),
ProxyFactoryClass: AttributeInfo.createType(ObjectReferenceEntity),
ProxyClass: AttributeInfo.createType(ObjectReferenceEntity),
StructType: AttributeInfo.createType(ObjectReferenceEntity),
MaterialExpression: AttributeInfo.createType(ObjectReferenceEntity),
MaterialExpressionComment: AttributeInfo.createType(ObjectReferenceEntity),
MoveMode: AttributeInfo.createType(SymbolEntity),
TimelineName: AttributeInfo.createType(String),
TimelineGuid: AttributeInfo.createType(GuidEntity),
SizeX: AttributeInfo.createType(new MirroredEntity(IntegerEntity)),
SizeY: AttributeInfo.createType(new MirroredEntity(IntegerEntity)),
Text: AttributeInfo.createType(new MirroredEntity(String)),
MaterialExpressionEditorX: AttributeInfo.createType(new MirroredEntity(IntegerEntity)),
MaterialExpressionEditorY: AttributeInfo.createType(new MirroredEntity(IntegerEntity)),
NodeTitle: AttributeInfo.createType(String),
NodeTitleColor: AttributeInfo.createType(LinearColorEntity),
PositionX: AttributeInfo.createType(new MirroredEntity(IntegerEntity)),
PositionY: AttributeInfo.createType(new MirroredEntity(IntegerEntity)),
SettingsInterface: AttributeInfo.createType(ObjectReferenceEntity),
PCGNode: AttributeInfo.createType(ObjectReferenceEntity),
HiGenGridSize: AttributeInfo.createType(SymbolEntity),
Operation: AttributeInfo.createType(SymbolEntity),
NodePosX: AttributeInfo.createType(IntegerEntity),
NodePosY: AttributeInfo.createType(IntegerEntity),
NodeHeight: AttributeInfo.createType(IntegerEntity),
NodeWidth: AttributeInfo.createType(IntegerEntity),
Graph: AttributeInfo.createType(ObjectReferenceEntity),
SubgraphInstance: AttributeInfo.createType(String),
InputPins: new AttributeInfo({
type: [ObjectReferenceEntity],
inlined: true,
}),
OutputPins: new AttributeInfo({
type: [ObjectReferenceEntity],
inlined: true,
}),
bExposeToLibrary: AttributeInfo.createType(Boolean),
bCanRenameNode: AttributeInfo.createType(Boolean),
bCommentBubblePinned: AttributeInfo.createType(Boolean),
bCommentBubbleVisible: AttributeInfo.createType(Boolean),
NodeComment: AttributeInfo.createType(String),
AdvancedPinDisplay: AttributeInfo.createType(IdentifierEntity),
DelegateReference: AttributeInfo.createType(VariableReferenceEntity),
EnabledState: AttributeInfo.createType(IdentifierEntity),
NodeGuid: AttributeInfo.createType(GuidEntity),
ErrorType: AttributeInfo.createType(IntegerEntity),
ErrorMsg: AttributeInfo.createType(String),
ScriptVariables: new AttributeInfo({
type: [ScriptVariableEntity],
inlined: true,
}),
Node: AttributeInfo.createType(new MirroredEntity(ObjectReferenceEntity)),
ExportedNodes: AttributeInfo.createType(String),
CustomProperties: AttributeInfo.createType([new Union(PinEntity, UnknownPinEntity)]),
}
static nameRegex = /^(\w+?)(?:_(\d+))?$/
static customPropertyGrammar = Parsernostrum.seq(
Parsernostrum.reg(/CustomProperties\s+/),
Grammar.grammarFor(
undefined,
this.attributes.CustomProperties.type[0]
),
).map(([_0, pin]) => values => {
if (!values.CustomProperties) {
values.CustomProperties = [];
}
values.CustomProperties.push(pin);
})
static inlinedArrayEntryGrammar = Parsernostrum.seq(
Parsernostrum.alt(
Grammar.symbolQuoted.map(v => [v, true]),
Grammar.symbol.map(v => [v, false]),
),
Parsernostrum.reg(
new RegExp(`\\s*\\(\\s*(\\d+)\\s*\\)\\s*\\=\\s*`),
1
).map(Number)
)
.chain(
/** @param {[[String, Boolean], Number]} param */
([[symbol, quoted], index]) =>
Grammar.grammarFor(this.attributes[symbol])
.map(currentValue =>
values => {
(values[symbol] ??= [])[index] = currentValue;
Utility.objectSet(values, ["attributes", symbol, "quoted"], quoted);
if (!this.attributes[symbol]?.inlined) {
if (!values.attributes) {
IEntity.defineAttributes(values, {});
}
Utility.objectSet(values, ["attributes", symbol, "type"], [currentValue.constructor]);
Utility.objectSet(values, ["attributes", symbol, "inlined"], true);
}
}
)
)
static grammar = this.createGrammar()
static createSubObjectGrammar() {
return Parsernostrum.lazy(() => this.grammar)
.map(object =>
values => values[Configuration.subObjectAttributeNameFromEntity(object)] = object
)
}
static createGrammar() {
return Parsernostrum.seq(
Parsernostrum.reg(/Begin +Object/),
Parsernostrum.seq(
Parsernostrum.whitespace,
Parsernostrum.alt(
this.createSubObjectGrammar(),
this.customPropertyGrammar,
Grammar.createAttributeGrammar(this, Parsernostrum.reg(Grammar.Regex.MultipleWordsSymbols)),
Grammar.createAttributeGrammar(this, Grammar.attributeNameQuoted, undefined, (obj, k, v) =>
Utility.objectSet(obj, ["attributes", ...k, "quoted"], true)
),
this.inlinedArrayEntryGrammar,
)
)
.map(([_0, entry]) => entry)
.many(),
Parsernostrum.reg(/\s+End +Object/),
)
.map(([_0, attributes, _2]) => {
const values = {};
attributes.forEach(attributeSetter => attributeSetter(values));
return new this(values)
})
}
static getMultipleObjectsGrammar() {
return Parsernostrum.seq(
Parsernostrum.whitespaceOpt,
this.grammar,
Parsernostrum.seq(
Parsernostrum.whitespace,
this.grammar,
)
.map(([_0, object]) => object)
.many(),
Parsernostrum.whitespaceOpt
)
.map(([_0, first, remaining, _4]) => [first, ...remaining])
}
/** @type {String} */
#class
constructor(values = {}, suppressWarns = false) {
if ("NodePosX" in values !== "NodePosY" in values) {
const entries = Object.entries(values);
const [key, position] = "NodePosX" in values
? ["NodePosY", Object.keys(values).indexOf("NodePosX") + 1]
: ["NodePosX", Object.keys(values).indexOf("NodePosY")];
const entry = [key, new (AttributeInfo.getAttribute(values, key, "type", ObjectEntity))()];
entries.splice(position, 0, entry);
values = Object.fromEntries(entries);
}
super(values, suppressWarns);
// Attributes not assigned a strong type in attributes because the names are too generic
/** @type {Number | MirroredEntity} */ this.R;
/** @type {Number | MirroredEntity} */ this.G;
/** @type {Number | MirroredEntity} */ this.B;
/** @type {Number | MirroredEntity} */ this.A;
// Attributes
/** @type {(PinEntity | UnknownPinEntity)[]} */ this.CustomProperties;
/** @type {Boolean} */ this.bIsPureFunc;
/** @type {Boolean} */ this.isExported;
/** @type {FunctionReferenceEntity} */ this.ComponentPropertyName;
/** @type {FunctionReferenceEntity} */ this.EventReference;
/** @type {FunctionReferenceEntity} */ this.FunctionReference;
/** @type {IdentifierEntity} */ this.AdvancedPinDisplay;
/** @type {IdentifierEntity} */ this.EnabledState;
/** @type {IntegerEntity} */ this.NodeHeight;
/** @type {IntegerEntity} */ this.NodePosX;
/** @type {IntegerEntity} */ this.NodePosY;
/** @type {IntegerEntity} */ this.NodeWidth;
/** @type {LinearColorEntity} */ this.CommentColor;
/** @type {LinearColorEntity} */ this.NodeTitleColor;
/** @type {MacroGraphReferenceEntity} */ this.MacroGraphReference;
/** @type {MirroredEntity} */ this.MaterialExpressionEditorX;
/** @type {MirroredEntity} */ this.MaterialExpressionEditorY;
/** @type {MirroredEntity} */ this.SizeX;
/** @type {MirroredEntity} */ this.SizeY;
/** @type {MirroredEntity} */ this.Text;
/** @type {MirroredEntity} */ this.PositionX;
/** @type {MirroredEntity} */ this.PositionY;
/** @type {MirroredEntity} */ this.Node;
/** @type {null[]} */ this.PinTags;
/** @type {Number} */ this.NumAdditionalInputs;
/** @type {ObjectReferenceEntity[]} */ this.InputPins;
/** @type {ObjectReferenceEntity[]} */ this.OutputPins;
/** @type {ObjectReferenceEntity} */ this.Archetype;
/** @type {ObjectReferenceEntity} */ this.BlueprintElementInstance;
/** @type {ObjectReferenceEntity} */ this.BlueprintElementType;
/** @type {ObjectReferenceEntity} */ this.Class;
/** @type {ObjectReferenceEntity} */ this.Enum;
/** @type {ObjectReferenceEntity} */ this.ExportPath;
/** @type {ObjectReferenceEntity} */ this.FunctionScript;
/** @type {ObjectReferenceEntity} */ this.Graph;
/** @type {ObjectReferenceEntity} */ this.MaterialExpression;
/** @type {ObjectReferenceEntity} */ this.MaterialExpressionComment;
/** @type {ObjectReferenceEntity} */ this.MaterialFunction;
/** @type {ObjectReferenceEntity} */ this.ObjectRef;
/** @type {ObjectReferenceEntity} */ this.PCGNode;
/** @type {ObjectReferenceEntity} */ this.SettingsInterface;
/** @type {ObjectReferenceEntity} */ this.StructType;
/** @type {ObjectReferenceEntity} */ this.TargetType;
/** @type {ScriptVariableEntity[]} */ this.ScriptVariables;
/** @type {String[]} */ this.EnumEntries;
/** @type {String[]} */ this.PinNames;
/** @type {String} */ this.CustomFunctionName;
/** @type {String} */ this.DelegatePropertyName;
/** @type {String} */ this.ExportedNodes;
/** @type {String} */ this.FunctionDisplayName;
/** @type {String} */ this.InputName;
/** @type {String} */ this.Name;
/** @type {String} */ this.NodeComment;
/** @type {String} */ this.NodeTitle;
/** @type {String} */ this.Operation;
/** @type {String} */ this.OpName;
/** @type {String} */ this.ProxyFactoryFunctionName;
/** @type {String} */ this.SubgraphInstance;
/** @type {String} */ this.Text;
/** @type {SymbolEntity} */ this.AxisKey;
/** @type {SymbolEntity} */ this.HiGenGridSize;
/** @type {SymbolEntity} */ this.InputAxisKey;
/** @type {SymbolEntity} */ this.InputKey;
/** @type {SymbolEntity} */ this.InputType;
/** @type {UnknownPinEntity[]} */ this.AddedPins;
/** @type {VariableReferenceEntity} */ this.DelegateReference;
/** @type {VariableReferenceEntity} */ this.VariableReference;
// Legacy nodes pins
if (this["Pins"] instanceof Array) {
this["Pins"].forEach(
/** @param {ObjectReferenceEntity} objectReference */
objectReference => {
const pinObject = this[Configuration.subObjectAttributeNameFromReference(objectReference, true)];
if (pinObject) {
const pinEntity = PinEntity.fromLegacyObject(pinObject);
pinEntity.LinkedTo = [];
this.getCustomproperties(true).push(pinEntity);
Utility.objectSet(this, ["attributes", "CustomProperties", "ignored"], true);
}
}
);
}
/** @type {ObjectEntity} */
const materialSubobject = this.getMaterialSubobject();
if (materialSubobject) {
const obj = materialSubobject;
obj.SizeX !== undefined && (obj.SizeX.getter = () => this.NodeWidth);
obj.SizeY && (obj.SizeY.getter = () => this.NodeHeight);
obj.Text && (obj.Text.getter = () => this.NodeComment);
obj.MaterialExpressionEditorX && (obj.MaterialExpressionEditorX.getter = () => this.NodePosX);
obj.MaterialExpressionEditorY && (obj.MaterialExpressionEditorY.getter = () => this.NodePosY);
if (this.getType() === Configuration.paths.materialExpressionComponentMask) {
// The following attributes are too generic therefore not assigned a MirroredEntity
const rgbaPins = Configuration.rgba.map(pinName =>
this.getPinEntities().find(pin => pin.PinName === pinName && (pin.recomputesNodeTitleOnChange = true))
);
obj.R = new MirroredEntity(Boolean, () => rgbaPins[0].DefaultValue);
obj.G = new MirroredEntity(Boolean, () => rgbaPins[1].DefaultValue);
obj.B = new MirroredEntity(Boolean, () => rgbaPins[2].DefaultValue);
obj.A = new MirroredEntity(Boolean, () => rgbaPins[3].DefaultValue);
Utility.objectSet(obj, ["attributes", "R", "default"], false);
Utility.objectSet(obj, ["attributes", "R", "silent"], true);
Utility.objectSet(obj, ["attributes", "G", "default"], false);
Utility.objectSet(obj, ["attributes", "G", "silent"], true);
Utility.objectSet(obj, ["attributes", "B", "default"], false);
Utility.objectSet(obj, ["attributes", "B", "silent"], true);
Utility.objectSet(obj, ["attributes", "A", "default"], false);
Utility.objectSet(obj, ["attributes", "A", "silent"], true);
obj._keys = [...Configuration.rgba, ...Object.keys(obj).filter(k => !Configuration.rgba.includes(k))];
}
}
/** @type {ObjectEntity} */
const pcgObject = this.getPcgSubobject();
if (pcgObject) {
pcgObject.PositionX && (pcgObject.PositionX.getter = () => this.NodePosX);
pcgObject.PositionY && (pcgObject.PositionY.getter = () => this.NodePosY);
pcgObject.getSubobjects().forEach(
/** @param {ObjectEntity} obj */
obj => {
if (obj.Node !== undefined) {
const nodeRef = obj.Node.get();
if (
nodeRef.type === this.PCGNode.type
&& nodeRef.path === `${this.Name}.${this.PCGNode.path}`
) {
obj.Node.getter = () => new ObjectReferenceEntity({
type: this.PCGNode.type,
path: `${this.Name}.${this.PCGNode.path}`,
});
}
}
}
);
}
let inputIndex = 0;
let outputIndex = 0;
this.CustomProperties?.forEach((pinEntity, i) => {
pinEntity.objectEntity = this;
pinEntity.pinIndex = pinEntity.isInput()
? inputIndex++
: pinEntity.isOutput()
? outputIndex++
: i;
});
}
getClass() {
if (!this.#class) {
this.#class = (this.Class?.path ? this.Class.path : this.Class?.type)
?? (this.ExportPath?.path ? this.ExportPath.path : this.ExportPath?.type)
?? "";
if (this.#class && !this.#class.startsWith("/")) {
// Old path names did not start with /Script or /Engine, check tests/resources/LegacyNodes.js
let path = Object.values(Configuration.paths).find(path => path.endsWith("." + this.#class));
if (path) {
this.#class = path;
}
}
}
return this.#class
}
getType() {
let classValue = this.getClass();
if (this.MacroGraphReference?.MacroGraph?.path) {
return this.MacroGraphReference.MacroGraph.path
}
if (this.MaterialExpression) {
return this.MaterialExpression.type
}
return classValue
}
getObjectName(dropCounter = false) {
if (dropCounter) {
return this.getNameAndCounter()[0]
}
return this.Name
}
/** @returns {[String, Number]} */
getNameAndCounter() {
const result = this.getObjectName().match(ObjectEntity.nameRegex);
return result
? [result[1] ?? "", parseInt(result[2] ?? "0")]
: ["", 0]
}
getCounter() {
return this.getNameAndCounter()[1]
}
getNodeWidth() {
return this.NodeWidth
?? this.isComment() ? Configuration.defaultCommentWidth : undefined
}
/** @param {Number} value */
setNodeWidth(value) {
if (!this.NodeWidth) {
this.NodeWidth = new IntegerEntity();
}
this.NodeWidth.value = value;
}
getNodeHeight() {
return this.NodeHeight
?? this.isComment() ? Configuration.defaultCommentHeight : undefined
}
/** @param {Number} value */
setNodeHeight(value) {
if (!this.NodeHeight) {
this.NodeHeight = new IntegerEntity();
}
this.NodeHeight.value = value;
}
getNodePosX() {
return this.NodePosX?.value ?? 0
}
/** @param {Number} value */
setNodePosX(value) {
if (!this.NodePosX) {
this.NodePosX = new IntegerEntity();
}
this.NodePosX.value = Math.round(value);
}
getNodePosY() {
return this.NodePosY?.value ?? 0
}
/** @param {Number} value */
setNodePosY(value) {
if (!this.NodePosY) {
this.NodePosY = new IntegerEntity();
}
this.NodePosY.value = Math.round(value);
}
getCustomproperties(canCreate = false) {
if (canCreate && !this.CustomProperties) {
this.CustomProperties = [];
}
return this.CustomProperties ?? []
}
/** @returns {PinEntity[]} */
getPinEntities() {
return this.getCustomproperties().filter(v => v.constructor === PinEntity)
}
/** @returns {ObjectEntity[]} */
getSubobjects() {
return Object.keys(this)
.filter(k => k.startsWith(Configuration.subObjectAttributeNamePrefix))
.flatMap(k => [this[k], .../** @type {ObjectEntity} */(this[k]).getSubobjects()])
}
switchTarget() {
const switchMatch = this.getClass().match(Configuration.switchTargetPattern);
if (switchMatch) {
return switchMatch[1]
}
}
isEvent() {
switch (this.getClass()) {
case Configuration.paths.actorBoundEvent:
case Configuration.paths.componentBoundEvent:
case Configuration.paths.customEvent:
case Configuration.paths.event:
case Configuration.paths.inputAxisKeyEvent:
case Configuration.paths.inputVectorAxisEvent:
return true
}
return false
}
isComment() {
switch (this.getClass()) {
case Configuration.paths.comment:
case Configuration.paths.materialGraphNodeComment:
return true
}
return false
}
isMaterial() {
return this.getClass() === Configuration.paths.materialGraphNode
// return [
// Configuration.paths.materialExpressionConstant,
// Configuration.paths.materialExpressionConstant2Vector,
// Configuration.paths.materialExpressionConstant3Vector,
// Configuration.paths.materialExpressionConstant4Vector,
// Configuration.paths.materialExpressionLogarithm,
// Configuration.paths.materialExpressionLogarithm10,
// Configuration.paths.materialExpressionLogarithm2,
// Configuration.paths.materialExpressionMaterialFunctionCall,
// Configuration.paths.materialExpressionSquareRoot,
// Configuration.paths.materialExpressionTextureCoordinate,
// Configuration.paths.materialExpressionTextureSample,
// Configuration.paths.materialGraphNode,
// Configuration.paths.materialGraphNodeComment,
// ]
// .includes(this.getClass())
}
/** @return {ObjectEntity} */
getMaterialSubobject() {
const expression = this.MaterialExpression ?? this.MaterialExpressionComment;
return expression
? this[Configuration.subObjectAttributeNameFromReference(expression, true)]
: null
}
isPcg() {
return this.getClass() === Configuration.paths.pcgEditorGraphNode
|| this.getPcgSubobject()
}
isNiagara() {
return this.Class && (this.Class.type ? this.Class.type : this.Class.path)?.startsWith("/Script/NiagaraEditor.")
}
/** @return {ObjectEntity} */
getPcgSubobject() {
const node = this.PCGNode;
return node
? this[Configuration.subObjectAttributeNameFromReference(node, true)]
: null
}
/** @return {ObjectEntity} */
getSettingsObject() {
const settings = this.SettingsInterface;
return settings
? this[Configuration.subObjectAttributeNameFromReference(settings, true)]
: null
}
/** @return {ObjectEntity} */
getSubgraphObject() {
const name = this.SubgraphInstance;
return name
? this[Configuration.subObjectAttributeNameFromName(name)]
: null
}
isDevelopmentOnly() {
const nodeClass = this.getClass();
return this.EnabledState?.toString() === "DevelopmentOnly"
|| nodeClass.includes("Debug", Math.max(0, nodeClass.lastIndexOf(".")))
}
getHIDAttribute() {
return this.InputKey ?? this.AxisKey ?? this.InputAxisKey
}
getDelegatePin() {
return this.getCustomproperties().find(pin => pin.PinType.PinCategory === "delegate")
}
nodeColor() {
return nodeColor(this)
}
nodeIcon() {
return nodeIcon(this)
}
additionalPinInserter() {
return nodeVariadic(this)
}
}
class KnotEntity extends ObjectEntity {
/**
* @param {Object} values
* @param {PinEntity} pinReferenceForType
*/
constructor(values = {}, pinReferenceForType = undefined) {
values.Class = new ObjectReferenceEntity(Configuration.paths.knot);
values.Name = "K2Node_Knot";
const inputPinEntity = new PinEntity(
{ PinName: "InputPin" },
true
);
const outputPinEntity = new PinEntity(
{
PinName: "OutputPin",
Direction: "EGPD_Output",
},
true
);
if (pinReferenceForType) {
inputPinEntity.copyTypeFrom(pinReferenceForType);
outputPinEntity.copyTypeFrom(pinReferenceForType);
}
values["CustomProperties"] = [inputPinEntity, outputPinEntity];
super(values, true);
}
}
/**
* @typedef {{
* consumeEvent?: Boolean,
* listenOnFocus?: Boolean,
* unlistenOnTextEdit?: Boolean,
* }} Options
*/
/** @template {Element} T */
class IInput {
/** @type {T} */
#target
get target() {
return this.#target
}
/** @type {Blueprint} */
#blueprint
get blueprint() {
return this.#blueprint
}
consumeEvent
/** @type {Object} */
options
listenHandler = () => this.listenEvents()
unlistenHandler = () => this.unlistenEvents()
/**
* @param {T} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}) {
options.consumeEvent ??= false;
options.listenOnFocus ??= false;
options.unlistenOnTextEdit ??= false;
this.#target = target;
this.#blueprint = blueprint;
this.consumeEvent = options.consumeEvent;
this.options = options;
}
setup() {
if (this.options.listenOnFocus) {
this.blueprint.addEventListener(Configuration.focusEventName.begin, this.listenHandler);
this.blueprint.addEventListener(Configuration.focusEventName.end, this.unlistenHandler);
}
if (this.options.unlistenOnTextEdit) {
this.blueprint.addEventListener(Configuration.editTextEventName.begin, this.unlistenHandler);
this.blueprint.addEventListener(Configuration.editTextEventName.end, this.listenHandler);
}
if (this.blueprint.focused) {
this.listenEvents();
}
}
cleanup() {
this.unlistenEvents();
this.blueprint.removeEventListener(Configuration.focusEventName.begin, this.listenHandler);
this.blueprint.removeEventListener(Configuration.focusEventName.end, this.unlistenHandler);
this.blueprint.removeEventListener(Configuration.editTextEventName.begin, this.unlistenHandler);
this.blueprint.removeEventListener(Configuration.editTextEventName.end, this.listenHandler);
}
/* Subclasses will probabily override the following methods */
listenEvents() {
}
unlistenEvents() {
}
}
class KeyBindingEntity extends IEntity {
static attributes = {
...super.attributes,
ActionName: AttributeInfo.createValue(""),
bShift: AttributeInfo.createValue(false),
bCtrl: AttributeInfo.createValue(false),
bAlt: AttributeInfo.createValue(false),
bCmd: AttributeInfo.createValue(false),
Key: AttributeInfo.createType(IdentifierEntity),
}
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.alt(
IdentifierEntity.grammar.map(identifier => new this({
Key: identifier
})),
Grammar.createEntityGrammar(this)
)
}
constructor(values = {}) {
super(values, true);
/** @type {String} */ this.ActionName;
/** @type {Boolean} */ this.bShift;
/** @type {Boolean} */ this.bCtrl;
/** @type {Boolean} */ this.bAlt;
/** @type {Boolean} */ this.bCmd;
/** @type {IdentifierEntity} */ this.Key;
}
}
/**
* @typedef {import("../IInput.js").Options & {
* activationKeys?: String | KeyBindingEntity | (String | KeyBindingEntity)[],
* consumeEvent?: Boolean,
* listenOnFocus?: Boolean,
* unlistenOnTextEdit?: Boolean,
* }} Options
*/
/**
* @template {Element} T
* @extends IInput
*/
class KeyboardShortcut extends IInput {
static #ignoreEvent =
/** @param {KeyboardShortcut} self */
self => { }
/** @type {KeyBindingEntity[]} */
#activationKeys
pressedKey = ""
/**
* @param {T} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(
target,
blueprint,
options = {},
onKeyDown = KeyboardShortcut.#ignoreEvent,
onKeyUp = KeyboardShortcut.#ignoreEvent
) {
options.activationKeys ??= [];
options.consumeEvent ??= true;
options.listenOnFocus ??= true;
options.unlistenOnTextEdit ??= true; // No shortcuts when inside of a text field
if (!(options.activationKeys instanceof Array)) {
options.activationKeys = [options.activationKeys];
}
options.activationKeys = options.activationKeys.map(v => {
if (v instanceof KeyBindingEntity) {
return v
}
if (v.constructor === String) {
const parsed = KeyBindingEntity.grammar.run(v);
if (parsed.status) {
return parsed.value
}
}
throw new Error("Unexpected key value")
});
super(target, blueprint, options);
this.onKeyDown = onKeyDown;
this.onKeyUp = onKeyUp;
this.#activationKeys = this.options.activationKeys ?? [];
const wantsShift = keyEntry => keyEntry.bShift || keyEntry.Key == "LeftShift" || keyEntry.Key == "RightShift";
const wantsCtrl = keyEntry => keyEntry.bCtrl || keyEntry.Key == "LeftControl" || keyEntry.Key == "RightControl";
const wantsAlt = keyEntry => keyEntry.bAlt || keyEntry.Key == "LeftAlt" || keyEntry.Key == "RightAlt";
let self = this;
/** @param {KeyboardEvent} e */
this.keyDownHandler = e => {
if (
self.#activationKeys.some(keyEntry =>
wantsShift(keyEntry) == e.shiftKey
&& wantsCtrl(keyEntry) == e.ctrlKey
&& wantsAlt(keyEntry) == e.altKey
&& Configuration.Keys[keyEntry.Key.value] == e.code
)
) {
if (this.consumeEvent) {
e.preventDefault();
e.stopImmediatePropagation();
}
this.pressedKey = e.code;
self.fire();
document.removeEventListener("keydown", self.keyDownHandler);
document.addEventListener("keyup", self.keyUpHandler);
}
};
/** @param {KeyboardEvent} e */
this.keyUpHandler = e => {
if (
self.#activationKeys.some(keyEntry =>
keyEntry.bShift && e.key == "Shift"
|| keyEntry.bCtrl && e.key == "Control"
|| keyEntry.bAlt && e.key == "Alt"
|| keyEntry.bCmd && e.key == "Meta"
|| Configuration.Keys[keyEntry.Key.value] == e.code
)
) {
if (this.consumeEvent) {
e.stopImmediatePropagation();
}
self.unfire();
this.pressedKey = "";
document.removeEventListener("keyup", this.keyUpHandler);
document.addEventListener("keydown", this.keyDownHandler);
}
};
}
listenEvents() {
document.addEventListener("keydown", this.keyDownHandler);
}
unlistenEvents() {
document.removeEventListener("keydown", this.keyDownHandler);
}
/* Subclasses can override */
fire() {
this.onKeyDown(this);
}
unfire() {
this.onKeyUp(this);
}
}
/**
* @typedef {import("../IInput.js").Options & {
* ignoreTranslateCompensate?: Boolean,
* ignoreScale?: Boolean,
* movementSpace?: HTMLElement,
* enablerKey?: KeyboardShortcut,
* }} Options
*/
/**
* @template {Element} T
* @extends {IInput}
*/
class IPointing extends IInput {
#location = /** @type {Coordinates} */([0, 0])
get location() {
return this.#location
}
/** @type {KeyboardShortcut?} */
#enablerKey
get enablerKey() {
return this.#enablerKey
}
#enablerActivated = true
get enablerActivated() {
return this.#enablerActivated
}
/**
* @param {T} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}) {
options.ignoreTranslateCompensate ??= false;
options.ignoreScale ??= false;
options.movementSpace ??= blueprint.getGridDOMElement() ?? document.documentElement;
super(target, blueprint, options);
/** @type {HTMLElement} */
this.movementSpace = options.movementSpace;
if (options.enablerKey) {
this.#enablerKey = options.enablerKey;
this.#enablerKey.onKeyDown = () => this.#enablerActivated = true;
this.#enablerKey.onKeyUp = () => this.#enablerActivated = false;
this.#enablerKey.consumeEvent = false;
this.#enablerKey.listenEvents();
this.#enablerActivated = false;
}
}
/** @param {MouseEvent} mouseEvent */
setLocationFromEvent(mouseEvent) {
let location = Utility.convertLocation(
[mouseEvent.clientX, mouseEvent.clientY],
this.movementSpace,
this.options.ignoreScale
);
location = this.options.ignoreTranslateCompensate
? location
: this.blueprint.compensateTranslation(location[0], location[1]);
this.#location = [...location];
return this.#location
}
}
/**
* @typedef {import("./IMouseClickDrag.js").Options & {
* }} Options
*/
/**
* @template {Element} T
* @extends {IPointing}
*/
class MouseClick extends IPointing {
static #ignoreEvent =
/** @param {MouseClick} self */
self => { }
/** @param {MouseEvent} e */
#mouseDownHandler = e => {
this.blueprint.setFocused(true);
if (this.enablerKey && !this.enablerActivated) {
return
}
switch (e.button) {
case this.options.clickButton:
// Either doesn't matter or consider the click only when clicking on the target, not descandants
if (!this.options.strictTarget || e.target === e.currentTarget) {
if (this.consumeEvent) {
e.stopImmediatePropagation(); // Captured, don't call anyone else
}
// Attach the listeners
document.addEventListener("mouseup", this.#mouseUpHandler);
this.setLocationFromEvent(e);
this.clickedPosition[0] = this.location[0];
this.clickedPosition[1] = this.location[1];
this.blueprint.mousePosition[0] = this.location[0];
this.blueprint.mousePosition[1] = this.location[1];
this.clicked(this.clickedPosition);
}
break
default:
if (!this.options.exitAnyButton) {
this.#mouseUpHandler(e);
}
break
}
}
/** @param {MouseEvent} e */
#mouseUpHandler = e => {
if (!this.options.exitAnyButton || e.button == this.options.clickButton) {
if (this.consumeEvent) {
e.stopImmediatePropagation(); // Captured, don't call anyone else
}
// Remove the handlers of "mousemove" and "mouseup"
document.removeEventListener("mouseup", this.#mouseUpHandler);
this.unclicked();
}
}
clickedPosition = [0, 0]
/**
* @param {T} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(
target,
blueprint,
options = {},
onClick = MouseClick.#ignoreEvent,
onUnclick = MouseClick.#ignoreEvent,
) {
options.clickButton ??= Configuration.mouseClickButton;
options.consumeEvent ??= true;
options.exitAnyButton ??= true;
options.strictTarget ??= false;
super(target, blueprint, options);
this.onClick = onClick;
this.onUnclick = onUnclick;
this.listenEvents();
}
listenEvents() {
this.target.addEventListener("mousedown", this.#mouseDownHandler);
if (this.options.clickButton === Configuration.mouseRightClickButton) {
this.target.addEventListener("contextmenu", e => e.preventDefault());
}
}
unlistenEvents() {
this.target.removeEventListener("mousedown", this.#mouseDownHandler);
}
/* Subclasses will override the following methods */
clicked(location) {
this.onClick(this);
}
unclicked(location) {
this.onUnclick(this);
}
}
/**
* @typedef {import("./IPointing.js").Options & {
* consumeEvent?: Boolean,
* strictTarget?: Boolean,
* }} Options
*/
/**
* @template {HTMLElement} T
* @extends {IPointing}
*/
class MouseDbClick extends IPointing {
/** @param {Coordinates} location */
static ignoreDbClick = location => { }
/** @param {MouseEvent} e */
#mouseDbClickHandler = e => {
if (!this.options.strictTarget || e.target === e.currentTarget) {
if (this.consumeEvent) {
e.stopImmediatePropagation(); // Captured, don't call anyone else
}
this.clickedPosition = this.setLocationFromEvent(e);
this.blueprint.mousePosition = [...this.clickedPosition];
this.dbclicked(this.clickedPosition);
}
}
#onDbClick
get onDbClick() {
return this.#onDbClick
}
set onDbClick(value) {
this.#onDbClick = value;
}
clickedPosition = /** @type {Coordinates} */([0, 0])
/**
* @param {T} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}, onDbClick = MouseDbClick.ignoreDbClick) {
options.consumeEvent ??= true;
options.strictTarget ??= false;
super(target, blueprint, options);
this.#onDbClick = onDbClick;
this.listenEvents();
}
listenEvents() {
this.target.addEventListener("dblclick", this.#mouseDbClickHandler);
}
unlistenEvents() {
this.target.removeEventListener("dblclick", this.#mouseDbClickHandler);
}
/* Subclasses will override the following method */
/** @param {Coordinates} location */
dbclicked(location) {
this.onDbClick(location);
}
}
/** @template {IElement} ElementT */
class ITemplate {
/** @type {ElementT} */
element
get blueprint() {
return this.element.blueprint
}
/** @type {IInput[]} */
#inputObjects = []
get inputObjects() {
return this.#inputObjects
}
/** @param {ElementT} element */
initialize(element) {
this.element = element;
}
createInputObjects() {
return /** @type {IInput[]} */([])
}
setup() {
this.#inputObjects.forEach(v => v.setup());
}
cleanup() {
this.#inputObjects.forEach(v => v.cleanup());
}
/** @param {PropertyValues} changedProperties */
willUpdate(changedProperties) {
}
/** @param {PropertyValues} changedProperties */
update(changedProperties) {
}
render() {
return x``
}
/** @param {PropertyValues} changedProperties */
firstUpdated(changedProperties) {
}
/** @param {PropertyValues} changedProperties */
updated(changedProperties) {
}
inputSetup() {
this.#inputObjects = this.createInputObjects();
}
}
/**
* @template {IFromToPositionedElement} T
* @extends {ITemplate}
*/
class IFromToPositionedTemplate extends ITemplate {
/** @param {PropertyValues} changedProperties */
update(changedProperties) {
super.update(changedProperties);
const [fromX, fromY, toX, toY] = [
Math.round(this.element.fromX),
Math.round(this.element.fromY),
Math.round(this.element.toX),
Math.round(this.element.toY),
];
const [left, top, width, height] = [
Math.min(fromX, toX),
Math.min(fromY, toY),
Math.abs(fromX - toX),
Math.abs(fromY - toY),
];
if (changedProperties.has("fromX") || changedProperties.has("toX")) {
this.element.style.left = `${left}px`;
this.element.style.width = `${width}px`;
}
if (changedProperties.has("fromY") || changedProperties.has("toY")) {
this.element.style.top = `${top}px`;
this.element.style.height = `${height}px`;
}
}
}
/** @extends {IFromToPositionedTemplate} */
class LinkTemplate extends IFromToPositionedTemplate {
/**
* Returns a function providing the inverse multiplication y = a / x + q. The value of a and q are calculated using
* the derivative of that function y' = -a / x^2 at the point p (x = p[0] and y = p[1]). This means
* y'(p[0]) = m => -a / p[0]^2 = m => a = -m * p[0]^2. Now, in order to determine q we can use the starting
* function: p[1] = a / p[0] + q => q = p[1] - a / p[0]
* @param {Number} m slope
* @param {Coordinates} p reference point
*/
static decreasingValue(m, p) {
const a = -m * p[0] ** 2;
const q = p[1] - a / p[0];
/** @param {Number} x */
return x => a / x + q
}
/**
* Returns a function providing a clamped line passing through two points. It is clamped after and before the
* points. It is easier explained with the following ascii draw.
* b ______
* /
* /
* /
* ______/ a
*/
static clampedLine(a, b) {
if (a[0] > b[0]) {
const temp = a;
a = b;
b = temp;
}
const m = (b[1] - a[1]) / (b[0] - a[0]);
const q = a[1] - m * a[0];
return x => x < a[0]
? a[1]
: x > b[0]
? b[1]
: m * x + q
}
static c1DecreasingValue = LinkTemplate.decreasingValue(-0.15, [100, 15])
static c2DecreasingValue = LinkTemplate.decreasingValue(-0.05, [500, 130])
static c2Clamped = LinkTemplate.clampedLine([0, 80], [200, 40])
#uniqueId = `ueb-id-${Math.floor(Math.random() * 1E12)}`
/** @param {Coordinates} location */
#createKnot = location => {
const knotEntity = new KnotEntity({}, this.element.source.entity);
const knot = /** @type {NodeElementConstructor} */(ElementFactory.getConstructor("ueb-node"))
.newObject(knotEntity);
knot.setLocation(...this.blueprint.snapToGrid(...location));
const knotTemplate = /** @type {KnotNodeTemplate} */(knot.template);
this.blueprint.addGraphElement(knot); // Important: keep it before changing existing links
const inputPin = this.element.getInputPin();
const outputPin = this.element.getOutputPin();
this.element.source = null;
this.element.destination = null;
const link = /** @type {LinkElementConstructor} */(ElementFactory.getConstructor("ueb-link"))
.newObject(outputPin, knotTemplate.inputPin);
this.blueprint.addGraphElement(link);
this.element.source = knotTemplate.outputPin;
this.element.destination = inputPin;
}
createInputObjects() {
/** @type {HTMLElement} */
const linkArea = this.element.querySelector(".ueb-link-area");
return [
...super.createInputObjects(),
new MouseDbClick(
linkArea,
this.blueprint,
undefined,
/** @param {Coordinates} location */
location => {
location[0] += Configuration.knotOffset[0];
location[1] += Configuration.knotOffset[1];
location = Utility.snapToGrid(location[0], location[1], Configuration.gridSize);
this.#createKnot(location);
},
),
new MouseClick(
linkArea,
this.blueprint,
{
enablerKey: new KeyboardShortcut(this.blueprint, this.blueprint, {
activationKeys: Shortcuts.enableLinkDelete,
})
},
() => this.blueprint.removeGraphElement(this.element),
),
]
}
/** @param {PropertyValues} changedProperties */
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
const sourcePin = this.element.source;
const destinationPin = this.element.destination;
if (changedProperties.has("fromX") || changedProperties.has("toX")) {
const from = this.element.fromX;
const to = this.element.toX;
const isSourceAKnot = sourcePin?.nodeElement.getType() == Configuration.paths.knot;
const isDestinationAKnot = destinationPin?.nodeElement.getType() == Configuration.paths.knot;
if (isSourceAKnot && (!destinationPin || isDestinationAKnot)) {
if (sourcePin?.isInput() && to > from + Configuration.distanceThreshold) {
this.element.source = /** @type {KnotNodeTemplate} */(sourcePin.nodeElement.template).outputPin;
} else if (sourcePin?.isOutput() && to < from - Configuration.distanceThreshold) {
this.element.source = /** @type {KnotNodeTemplate} */(sourcePin.nodeElement.template).inputPin;
}
}
if (isDestinationAKnot && (!sourcePin || isSourceAKnot)) {
if (destinationPin?.isInput() && to < from - Configuration.distanceThreshold) {
this.element.destination =
/** @type {KnotNodeTemplate} */(destinationPin.nodeElement.template).outputPin;
} else if (destinationPin?.isOutput() && to > from + Configuration.distanceThreshold) {
this.element.destination =
/** @type {KnotNodeTemplate} */(destinationPin.nodeElement.template).inputPin;
}
}
}
const dx = Math.max(Math.abs(this.element.fromX - this.element.toX), 1);
const dy = Math.max(Math.abs(this.element.fromY - this.element.toY), 1);
const width = Math.max(dx, Configuration.linkMinWidth);
// const height = Math.max(Math.abs(link.fromY - link.toY), 1)
const fillRatio = dx / width;
const xInverted = this.element.originatesFromInput
? this.element.fromX < this.element.toX
: this.element.toX < this.element.fromX;
this.element.startPixels = dx < width // If under minimum width
? (width - dx) / 2 // Start from half the empty space
: 0; // Otherwise start from the beginning
this.element.startPercentage = xInverted ? this.element.startPixels + fillRatio * 100 : this.element.startPixels;
const c1 =
this.element.startPercentage
+ (xInverted
? LinkTemplate.c1DecreasingValue(width)
: 10
)
* fillRatio;
const aspectRatio = dy / Math.max(30, dx);
const c2 =
LinkTemplate.c2Clamped(dx)
* Utility.sigmoidPositive(fillRatio * 1.2 + aspectRatio * 0.5, 1.5, 1.8)
+ this.element.startPercentage;
this.element.svgPathD = Configuration.linkRightSVGPath(this.element.startPercentage, c1, c2);
}
/** @param {PropertyValues} changedProperties */
update(changedProperties) {
super.update(changedProperties);
if (changedProperties.has("originatesFromInput")) {
this.element.style.setProperty("--ueb-from-input", this.element.originatesFromInput ? "1" : "0");
}
const referencePin = this.element.source ?? this.element.destination;
if (referencePin) {
this.element.style.setProperty("--ueb-link-color-rgb", Utility.printLinearColor(referencePin.color));
}
this.element.style.setProperty("--ueb-y-reflected", `${this.element.fromY > this.element.toY ? 1 : 0}`);
this.element.style.setProperty("--ueb-start-percentage", `${Math.round(this.element.startPercentage)}%`);
this.element.style.setProperty("--ueb-link-start", `${Math.round(this.element.startPixels)}`);
}
render() {
return x`
${this.element.linkMessageIcon || this.element.linkMessageText ? x`
${this.element.linkMessageIcon !== A ? x`
${this.element.linkMessageIcon}
` : A}
${this.element.linkMessageText !== A ? x`
${this.element.linkMessageText}
` : A}
` : A}
`
}
}
/** @extends {IFromToPositionedElement