} parser */
constructor(parser) {
super();
this.#parser = parser;
}
resolve() {
if (!this.#resolvedPraser) {
this.#resolvedPraser = this.#parser().getParser();
}
return this.#resolvedPraser
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
*/
parse(context, position, path) {
this.resolve();
return this.#resolvedPraser.parse(context, position, { parent: path, parser: this.#resolvedPraser, index: 0 })
}
/**
* @protected
* @param {Context} context
* @param {Number} indent
* @param {PathNode} path
*/
doToString(context, indent, path) {
const childrenPath = { parent: path, parser: this.#resolvedPraser, index: 0 };
if (this.isHighlighted(context, path)) {
context.highlighted = context.highlighted instanceof Parser ? this.#resolvedPraser : childrenPath;
}
return this.resolve().toString(context, indent, childrenPath)
}
}
/** @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
*/
parse(context, position, path) {
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 {Number} indent
* @param {PathNode} path
*/
doToString(context, indent, path) {
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;
}
if (this.isHighlighted(context, path)) {
result += "\n" + Parser.indentation.repeat(indent) + "^".repeat(result.length) + " " + Parser.highlight;
}
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 {Number} position
* @param {PathNode} path
* @returns {Result
}
*/
parse(context, position, path) {
const result = this.#parser.parse(context, position, { parent: path, parser: this.#parser, index: 0 });
if (result.status) {
result.value = this.#mapper(result.value);
}
return result
}
/**
* @protected
* @param {Context} context
* @param {Number} indent
* @param {PathNode} path
*/
doToString(context, indent, path) {
const childrenPath = { parent: path, parser: this.#parser, index: 0 };
if (this.isHighlighted(context, path)) {
context.highlighted = context.highlighted instanceof Parser ? this.#parser : childrenPath;
}
let result = this.#parser.toString(context, indent, childrenPath);
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 = "(...) => { ... }";
}
serializedMapper = ` -> map<${serializedMapper}>`;
result = Parser.appendBeforeHighlight(result, 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
*/
parse(context, position, path) {
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,
{ parent: path, parser: this.#parsers[i], index: 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 {Number} indent
* @param {PathNode} path
*/
doToString(context, indent, path) {
const indentation = Parser.indentation.repeat(indent);
const deeperIndentation = Parser.indentation.repeat(indent + 1);
const result = "SEQ<\n"
+ (this.isHighlighted(context, path) ? `${indentation}^^^ ${Parser.highlight}\n` : "")
+ this.#parsers
.map((parser, index) => deeperIndentation + parser.toString(context, indent + 1, { parent: path, parser, index }))
.join("\n")
+ "\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
*/
parse(context, position, path) {
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,
{ parent: path, parser: this.#parser, index: 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 {Number} indent
* @param {PathNode} path
*/
doToString(context, indent, path) {
let result = this.parser.toString(context, indent, { parent: path, parser: this.parser, index: 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 : "")
+ "}";
if (this.isHighlighted(context, path)) {
result +=
serialized
+ "\n"
+ " ".repeat(Parser.lastRowLength(result, Parser.indentation.length * indent))
+ "^".repeat(serialized.length)
+ " "
+ Parser.highlight;
} else {
result = Parser.appendBeforeHighlight(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;
}
getParser() {
return this.#parser
}
/**
* @param {String} input
* @returns {Result>}
*/
run(input) {
const result = this.#parser.parse(Reply.makeContext(this, input), 0, Reply.makePathNode(this.#parser));
if (result.position !== input.length) {
result.status = false;
}
return result
}
/**
* @param {String} input
* @throws {Error} when the parser fails to match
*/
parse(input) {
const result = this.run(input);
if (!result.status) {
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 + "...";
}
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\n`
+ (result.bestParser ? "Last valid parser matched:" : "No parser matched:")
+ this.toString(1, true, result.bestParser)
+ "\n"
)
}
return result.value
}
// 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, Parsernostrum, ...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)
}
/** @param {Parsernostrum | Parser | PathNode} highlight */
toString(indent = 0, newline = false, highlight = null) {
if (highlight instanceof Parsernostrum) {
highlight = highlight.getParser();
}
const context = Reply.makeContext(this, "");
context.highlighted = highlight;
return (newline ? "\n" + Parser.indentation.repeat(indent) : "")
+ this.#parser.toString(context, indent, Reply.makePathNode(this.#parser))
}
}
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 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`
`
}
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,
}),
}
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) {
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;
}
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 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.DotSeparatedSymbols.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
* @returns {Parsernostrum}
*/
static grammarFor(attribute, type = attribute?.type, defaultGrammar = this.unknownValue) {
let result = defaultGrammar;
if (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('"'));
}
}
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))
})
}
/* --- Entity --- */
static unknownValue // Defined in initializeSerializerFactor to avoid circular include
}
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 #quoteSymbols = [
[`'"`, Grammar.Regex.InsideString.source],
[`'`, Grammar.Regex.InsideSingleQuotedString.source],
[`"`, Grammar.Regex.InsideString.source]
]
static attributes = {
...super.attributes,
type: new AttributeInfo({
default: "",
serialized: true,
}),
path: new AttributeInfo({
default: "",
serialized: true,
}),
delim: new AttributeInfo({
ignored: true,
}),
}
static quoted = Parsernostrum.regArray(new RegExp(
this.#quoteSymbols.map(([delim, parser]) =>
delim + "(" + parser + ")" + delim.split("").reverse().join("")).join("|")
)).map(([_0, a, b, c]) => a ?? b ?? c)
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 + ")"
+ /\s*/.source
+ "(?:" + this.quoted.getParser().parser.regexp.source + ")"
)
).map(([_0, type, ...path]) => new this({
type,
path: path.find(v => v),
delim: this.#quoteSymbols[path.findIndex(v => v)]?.[0] ?? "",
}))
static fullReferenceSerializedGrammar = Parsernostrum.regArray(
new RegExp(
"(" + this.typeReference.getParser().regexp.source + ")"
+ /\s*/.source
+ `'(` + Grammar.Regex.InsideSingleQuotedString.source + `)'`
)
).map(([_0, type, ...path]) => new this({
type,
path: path.find(v => v),
delim: "'",
}))
static typeReferenceGrammar = this.typeReference.map(v => new this({ type: v, path: "" }))
static grammar = this.createGrammar()
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.seq(
Parsernostrum.str('"'),
Parsernostrum.alt(
this.fullReferenceSerializedGrammar,
this.typeReferenceGrammar,
),
Parsernostrum.str('"'),
).map(([_0, objectReference, _1]) => objectReference),
this.fullReferenceGrammar.map(v => (Utility.objectSet(v, ["attributes", "type", "serialized"], false), v)),
this.typeReferenceGrammar.map(v => (Utility.objectSet(v, ["attributes", "type", "serialized"], false), v)),
)
}
constructor(values = {}) {
if (values.constructor === String) {
values = {
path: values
};
}
super(values);
/** @type {String} */ this.type;
/** @type {String} */ this.path;
/** @type {String} */ this.delim;
}
static createNoneInstance() {
return new ObjectReferenceEntity({ type: "None", path: "" })
}
getName() {
return Utility.getNameFromPath(this.path.replace(/_C$/, ""))
}
toString() {
return this.type + (this.path ? (this.delim + this.path + this.delim.split("").reverse().join("")) : "")
}
}
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 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)
}
}
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)
}
}
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;
}
}
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.createGrammar(),
Parsernostrum.whitespace,
GuidEntity.createGrammar()
).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.createGrammar()
)
}
}
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.createGrammar()
)
}
}
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.createGrammar()
)
}
}
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.createGrammar()
)
}
}
/** @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,
"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,
}
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, 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;
if (category === "struct" || category === "object") {
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
}
pinDisplayName() {
let result = this.PinFriendlyName
? this.PinFriendlyName.toString()
: Utility.formatStringName(this.PinName ?? "");
let match;
if (
this.PinToolTip
// Match up until the first \n excluded or last character
&& (match = this.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
}
/** @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
}
/** @return {CSSResult} */
pinColor() {
if (this.PinType.PinCategory == "mask") {
const result = Configuration.pinColor[this.PinType.PinSubCategory];
if (result) {
return result
}
} else if (this.PinType.PinCategory == "optional") {
return Configuration.pinColorMaterial
}
return Configuration.pinColor[this.getType()]
?? Configuration.pinColor[this.PinType.PinCategory.toLowerCase()]
?? Configuration.pinColor["default"]
}
}
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;
}
}
class ObjectEntity extends IEntity {
static #keyName = {
"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": "`",
}
static attributes = {
...super.attributes,
Class: AttributeInfo.createType(ObjectReferenceEntity),
Name: AttributeInfo.createType(String),
Archetype: AttributeInfo.createType(ObjectReferenceEntity),
ExportPath: AttributeInfo.createType(ObjectReferenceEntity),
R: new AttributeInfo({
type: new Union(Boolean, Number),
default: false,
silent: true,
}),
G: new AttributeInfo({
type: new Union(Boolean, Number),
default: false,
silent: true,
}),
B: new AttributeInfo({
type: new Union(Boolean, Number),
default: false,
silent: true,
}),
A: new AttributeInfo({
type: new Union(Boolean, Number),
default: false,
silent: true,
}),
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),
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),
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),
Node: AttributeInfo.createType(new MirroredEntity(ObjectReferenceEntity)),
CustomProperties: AttributeInfo.createType([new Union(PinEntity, UnknownPinEntity)]),
}
static nameRegex = /^(\w+?)(?:_(\d+))?$/
static sequencerScriptingNameRegex = /\/Script\/SequencerScripting\.MovieSceneScripting(.+)Channel/
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\s+Object/),
Parsernostrum.seq(
Parsernostrum.whitespace,
Parsernostrum.alt(
this.customPropertyGrammar,
Grammar.createAttributeGrammar(this),
Grammar.createAttributeGrammar(this, Grammar.attributeNameQuoted, undefined, (obj, k, v) =>
Utility.objectSet(obj, ["attributes", ...k, "quoted"], true)
),
this.inlinedArrayEntryGrammar,
this.createSubObjectGrammar()
)
)
.map(([_0, entry]) => entry)
.many(),
Parsernostrum.reg(/\s+End\s+Object/),
)
.map(([_0, attributes, _2]) => {
const values = {};
attributes.forEach(attributeSetter => attributeSetter(values));
return new this(values)
})
}
/** @param {String} value */
static keyName(value) {
/** @type {String} */
let result = ObjectEntity.#keyName[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
}
}
}
static getMultipleObjectsGrammar() {
return Parsernostrum.seq(
Parsernostrum.whitespaceOpt,
this.createGrammar(),
Parsernostrum.seq(
Parsernostrum.whitespace,
this.createGrammar(),
)
.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 {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.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 {String[]} */ this.EnumEntries;
/** @type {String[]} */ this.PinNames;
/** @type {String} */ this.CustomFunctionName;
/** @type {String} */ this.DelegatePropertyName;
/** @type {String} */ this.InputName;
/** @type {String} */ this.Name;
/** @type {String} */ this.NodeComment;
/** @type {String} */ this.NodeTitle;
/** @type {String} */ this.Operation;
/** @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 {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);
}
}
/** @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(false).match(ObjectEntity.nameRegex);
let name = "";
let counter = null;
if (result) {
if (result.length > 1) {
name = result[1];
}
if (result.length > 2) {
counter = parseInt(result[2]);
}
return [name, counter]
}
return ["", 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()
}
/** @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 node = this.SubgraphInstance;
return node
? this[Configuration.subObjectAttributeNameFromName(node)]
: 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")
}
/** @returns {String} */
nodeDisplayName() {
let input;
switch (this.getType()) {
case Configuration.paths.asyncAction:
if (this.ProxyFactoryFunctionName) {
return Utility.formatStringName(this.ProxyFactoryFunctionName)
}
case Configuration.paths.actorBoundEvent:
case Configuration.paths.componentBoundEvent:
return `${Utility.formatStringName(this.DelegatePropertyName)} (${this.ComponentPropertyName ?? "Unknown"})`
case Configuration.paths.callDelegate:
return `Call ${this.DelegateReference?.MemberName ?? "None"}`
case Configuration.paths.createDelegate:
return "Create Event"
case Configuration.paths.customEvent:
if (this.CustomFunctionName) {
return this.CustomFunctionName
}
case Configuration.paths.dynamicCast:
if (!this.TargetType) {
return "Bad cast node" // Target type not found
}
return `Cast To ${this.TargetType?.getName()}`
case Configuration.paths.enumLiteral:
return `Literal enum ${this.Enum?.getName()}`
case Configuration.paths.event:
return `Event ${(this.EventReference?.MemberName ?? "").replace(/^Receive/, "")}`
case Configuration.paths.executionSequence:
return "Sequence"
case Configuration.paths.forEachElementInEnum:
return `For Each ${this.Enum?.getName()}`
case Configuration.paths.forEachLoopWithBreak:
return "For Each Loop with Break"
case Configuration.paths.functionEntry:
return this.FunctionReference?.MemberName === "UserConstructionScript"
? "Construction Script"
: this.FunctionReference?.MemberName
case Configuration.paths.functionResult:
return "Return Node"
case Configuration.paths.ifThenElse:
return "Branch"
case Configuration.paths.makeStruct:
if (this.StructType) {
return `Make ${this.StructType.getName()}`
}
case Configuration.paths.materialExpressionComponentMask: {
const materialObject = this.getMaterialSubobject();
return `Mask ( ${Configuration.rgba
.filter(k => /** @type {MirroredEntity} */(materialObject[k]).get() === true)
.map(v => v + " ")
.join("")})`
}
case Configuration.paths.materialExpressionConstant:
input ??= [this.getCustomproperties().find(pinEntity => pinEntity.PinName == "Value")?.DefaultValue];
case Configuration.paths.materialExpressionConstant2Vector:
input ??= [
this.getCustomproperties().find(pinEntity => pinEntity.PinName == "X")?.DefaultValue,
this.getCustomproperties().find(pinEntity => pinEntity.PinName == "Y")?.DefaultValue,
];
case Configuration.paths.materialExpressionConstant3Vector:
if (!input) {
/** @type {VectorEntity} */
const vector = this.getCustomproperties()
.find(pinEntity => pinEntity.PinName == "Constant")
?.DefaultValue;
input = [vector.X, vector.Y, vector.Z];
}
case Configuration.paths.materialExpressionConstant4Vector:
if (!input) {
/** @type {LinearColorEntity} */
const vector = this.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 = this.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 = this.getMaterialSubobject()?.MaterialFunction;
if (materialFunction) {
return materialFunction.getName()
}
break
case Configuration.paths.materialExpressionSquareRoot:
return "Sqrt"
case Configuration.paths.pcgEditorGraphNodeInput:
return "Input"
case Configuration.paths.pcgEditorGraphNodeOutput:
return "Output"
case Configuration.paths.spawnActorFromClass:
return `SpawnActor ${Utility.formatStringName(
this.getCustomproperties().find(pinEntity => pinEntity.getType() == "class")?.DefaultObject?.getName()
?? "NONE"
)}`
case Configuration.paths.switchEnum:
return `Switch on ${this.Enum?.getName() ?? "Enum"}`
case Configuration.paths.switchInteger:
return `Switch on Int`
case Configuration.paths.variableGet:
return ""
case Configuration.paths.variableSet:
return "SET"
}
let switchTarget = this.switchTarget();
if (switchTarget) {
if (switchTarget[0] !== "E") {
switchTarget = Utility.formatStringName(switchTarget);
}
return `Switch on ${switchTarget}`
}
if (this.isComment()) {
return this.NodeComment
}
const keyNameSymbol = this.getHIDAttribute();
if (keyNameSymbol) {
const keyName = keyNameSymbol.toString();
let title = ObjectEntity.keyName(keyName) ?? Utility.formatStringName(keyName);
if (this.getClass() === Configuration.paths.inputDebugKey) {
title = "Debug Key " + title;
} else if (this.getClass() === Configuration.paths.getInputAxisKeyValue) {
title = "Get " + title;
}
return title
}
if (this.getClass() === Configuration.paths.macro) {
return Utility.formatStringName(this.MacroGraphReference?.getMacroName())
}
if (this.isMaterial() && this.getMaterialSubobject()) {
let result = this.getMaterialSubobject().nodeDisplayName();
result = result.match(/Material Expression (.+)/)?.[1] ?? result;
return result
}
if (this.isPcg() && this.getPcgSubobject()) {
let pcgSubobject = this.getPcgSubobject();
let result = pcgSubobject.NodeTitle ? pcgSubobject.NodeTitle : pcgSubobject.nodeDisplayName();
return result
}
const subgraphObject = this.getSubgraphObject();
if (subgraphObject) {
return subgraphObject.Graph.getName()
}
const settingsObject = this.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 = this.FunctionReference?.MemberName;
if (memberName) {
const memberParent = this.FunctionReference.MemberParent?.path ?? "";
switch (memberName) {
case "AddKey":
let result = memberParent.match(ObjectEntity.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 (this.ObjectRef) {
return this.ObjectRef.getName()
}
return Utility.formatStringName(this.getNameAndCounter()[0])
}
nodeColor() {
switch (this.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 (this.getClass()) {
case Configuration.paths.callFunction:
return this.bIsPureFunc
? Configuration.nodeColors.green
: Configuration.nodeColors.blue
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 (this.switchTarget()) {
return Configuration.nodeColors.lime
}
if (this.isEvent()) {
return Configuration.nodeColors.red
}
if (this.isComment()) {
return (this.CommentColor ? this.CommentColor : LinearColorEntity.getWhite())
.toDimmedColor()
.toCSSRGBValues()
}
const pcgSubobject = this.getPcgSubobject();
if (pcgSubobject && pcgSubobject.NodeTitleColor) {
return pcgSubobject.NodeTitleColor.toDimmedColor(0.1).toCSSRGBValues()
}
if (this.bIsPureFunc) {
return Configuration.nodeColors.green
}
return Configuration.nodeColors.blue
}
nodeIcon() {
if (this.isMaterial() || this.isPcg()) {
return null
}
switch (this.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.select: return SVGIcon.select
case Configuration.paths.spawnActorFromClass: return SVGIcon.spawnActor
case Configuration.paths.timeline: return SVGIcon.timer
}
if (this.switchTarget()) {
return SVGIcon.switch
}
if (this.nodeDisplayName().startsWith("Break")) {
return SVGIcon.breakStruct
}
if (this.getClass() === Configuration.paths.macro) {
return SVGIcon.macro
}
const hidValue = this.getHIDAttribute()?.toString();
if (hidValue) {
if (hidValue.includes("Mouse")) {
return SVGIcon.mouse
} else if (hidValue.includes("Gamepad_Special")) {
return SVGIcon.keyboard // This 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 (this.getDelegatePin()) {
return SVGIcon.event
}
if (this.ObjectRef?.type === Configuration.paths.ambientSound) {
return SVGIcon.sound
}
return SVGIcon.functionSymbol
}
additionalPinInserter() {
/** @type {() => PinEntity[]} */
let pinEntities;
/** @type {(pinEntity: PinEntity) => Number} */
let pinIndexFromEntity;
/** @type {(newPinIndex: Number, minIndex: Number, maxIndex: Number) => String} */
let pinNameFromIndex;
switch (this.getType()) {
case Configuration.paths.commutativeAssociativeBinaryOperator:
case Configuration.paths.promotableOperator:
switch (this.FunctionReference?.MemberName) {
default:
if (
!this.FunctionReference?.MemberName?.startsWith("Add_")
&& !this.FunctionReference?.MemberName?.startsWith("Subtract_")
&& !this.FunctionReference?.MemberName?.startsWith("Multiply_")
&& !this.FunctionReference?.MemberName?.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 ??= () => this.getPinEntities().filter(pinEntity => pinEntity.isInput());
pinIndexFromEntity ??= pinEntity =>
pinEntity.PinName.match(/^\s*([A-Z])\s*$/)?.[1]?.charCodeAt(0) - "A".charCodeAt(0);
pinNameFromIndex ??= (index, min = -1, max = -1) => {
const result = String.fromCharCode(index >= 0 ? index : max + "A".charCodeAt(0) + 1);
this.NumAdditionalInputs = pinEntities().length - 1;
return result
};
break
}
break
case Configuration.paths.multiGate:
pinEntities ??= () => this.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.match(/^\s*Out[_\s]+(\d+)\s*$/i)?.[1]);
pinNameFromIndex ??= (index, min = -1, max = -1) =>
`Out ${index >= 0 ? index : min > 0 ? "Out 0" : max + 1}`;
break
case Configuration.paths.switchInteger:
pinEntities ??= () => this.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.match(/^\s*(\d+)\s*$/)?.[1]);
pinNameFromIndex ??= (index, min = -1, max = -1) => (index < 0 ? max + 1 : index).toString();
break
case Configuration.paths.switchGameplayTag:
pinNameFromIndex ??= (index, min = -1, max = -1) => {
const result = `Case_${index >= 0 ? index : min > 0 ? "0" : max + 1}`;
this.PinNames ??= [];
this.PinNames.push(result);
delete this.PinTags[this.PinTags.length - 1];
this.PinTags[this.PinTags.length] = null;
return result
};
case Configuration.paths.switchName:
case Configuration.paths.switchString:
pinEntities ??= () => this.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.match(/^\s*Case[_\s]+(\d+)\s*$/i)?.[1]);
pinNameFromIndex ??= (index, min = -1, max = -1) => {
const result = `Case_${index >= 0 ? index : min > 0 ? "0" : max + 1}`;
this.PinNames ??= [];
this.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.PinToolTip = undefined;
this.getCustomproperties(true).push(newPin);
return newPin
}
}
}
}
/** @template {AttributeConstructor} T */
class Serializer {
/** @type {(v: String) => String} */
static same = v => v
/** @type {(entity: Attribute, serialized: String) => String} */
static notWrapped = (entity, serialized) => serialized
/** @type {(entity: Attribute, serialized: String) => String} */
static bracketsWrapped = (entity, serialized) => `(${serialized})`
/** @param {T} entityType */
constructor(
entityType,
/** @type {(entity: ConstructedType, serialized: String) => String} */
wrap = (entity, serialized) => serialized,
attributeSeparator = ",",
trailingSeparator = false,
attributeValueConjunctionSign = "=",
attributeKeyPrinter = Serializer.same
) {
this.entityType = entityType;
this.wrap = wrap;
this.attributeSeparator = attributeSeparator;
this.trailingSeparator = trailingSeparator;
this.attributeValueConjunctionSign = attributeValueConjunctionSign;
this.attributeKeyPrinter = attributeKeyPrinter;
}
/**
* @param {String} value
* @returns {ConstructedType}
*/
read(value) {
return this.doRead(value.trim())
}
/** @param {ConstructedType} value */
write(value, insideString = false) {
return this.doWrite(value, insideString)
}
/**
* @param {String} value
* @returns {ConstructedType}
*/
doRead(value) {
let grammar = Grammar.grammarFor(undefined, this.entityType);
const parseResult = grammar.run(value);
if (!parseResult.status) {
throw new Error(
this.entityType
? `Error when trying to parse the entity ${this.entityType.prototype.constructor.name}`
: "Error when trying to parse null"
)
}
return parseResult.value
}
/**
* @param {ConstructedType} entity
* @param {Boolean} insideString
* @returns {String}
*/
doWrite(
entity,
insideString = false,
indentation = "",
wrap = this.wrap,
attributeSeparator = this.attributeSeparator,
trailingSeparator = this.trailingSeparator,
attributeValueConjunctionSign = this.attributeValueConjunctionSign,
attributeKeyPrinter = this.attributeKeyPrinter
) {
let result = "";
const keys = Object.keys(entity);
let first = true;
for (const key of keys) {
const value = entity[key];
if (value !== undefined && this.showProperty(entity, key)) {
let keyValue = entity instanceof Array ? `(${key})` : key;
if (AttributeInfo.getAttribute(entity, key, "quoted")) {
keyValue = `"${keyValue}"`;
}
const isSerialized = AttributeInfo.getAttribute(entity, key, "serialized");
if (first) {
first = false;
} else {
result += attributeSeparator;
}
if (AttributeInfo.getAttribute(entity, key, "inlined")) {
result += this.doWrite(
value,
insideString,
indentation,
Serializer.notWrapped,
attributeSeparator,
false,
attributeValueConjunctionSign,
AttributeInfo.getAttribute(entity, key, "type") instanceof Array
? k => attributeKeyPrinter(`${keyValue}${k}`)
: k => attributeKeyPrinter(`${keyValue}.${k}`)
);
continue
}
const keyPrinted = attributeKeyPrinter(keyValue);
const indentationPrinted = attributeSeparator.includes("\n") ? indentation : "";
result += (
keyPrinted.length
? (indentationPrinted + keyPrinted + this.attributeValueConjunctionSign)
: ""
)
+ (
isSerialized
? `"${this.doWriteValue(value, true, indentation)}"`
: this.doWriteValue(value, insideString, indentation)
);
}
}
if (trailingSeparator && result.length) {
// append separator at the end if asked and there was printed content
result += attributeSeparator;
}
return wrap(entity, result)
}
/** @param {Boolean} insideString */
doWriteValue(value, insideString, indentation = "") {
const type = Utility.getType(value);
const serializer = SerializerFactory.getSerializer(type);
if (!serializer) {
throw new Error(
`Unknown value type "${type.name}", a serializer must be registered in the SerializerFactory class, `
+ "check initializeSerializerFactory.js"
)
}
return serializer.doWrite(value, insideString, indentation)
}
/**
* @param {IEntity} entity
* @param {String} key
*/
showProperty(entity, key) {
if (entity instanceof IEntity) {
if (
AttributeInfo.getAttribute(entity, key, "ignored")
|| AttributeInfo.getAttribute(entity, key, "silent") && Utility.equals(
AttributeInfo.getAttribute(entity, key, "default"),
entity[key]
)
) {
return false
}
}
return true
}
}
/** @extends Serializer */
class ObjectSerializer extends Serializer {
constructor(entityType = ObjectEntity) {
super(entityType, undefined, "\n", true, undefined, Serializer.same);
}
showProperty(entity, key) {
switch (key) {
case "Class":
case "Name":
case "Archetype":
case "ExportPath":
case "CustomProperties":
// Serielized separately, check doWrite()
return false
}
return super.showProperty(entity, key)
}
/** @param {ObjectEntity} value */
write(value, insideString = false) {
return this.doWrite(value, insideString) + "\n"
}
/** @param {String} value */
doRead(value) {
return Grammar.grammarFor(undefined, this.entityType).parse(value)
}
/**
* @param {String} value
* @returns {ObjectEntity[]}
*/
readMultiple(value) {
return ObjectEntity.getMultipleObjectsGrammar().parse(value)
}
/**
* @param {ObjectEntity} entity
* @param {Boolean} insideString
* @returns {String}
*/
doWrite(
entity,
insideString,
indentation = "",
wrap = this.wrap,
attributeSeparator = this.attributeSeparator,
trailingSeparator = this.trailingSeparator,
attributeValueConjunctionSign = this.attributeValueConjunctionSign,
attributeKeyPrinter = this.attributeKeyPrinter,
) {
const moreIndentation = indentation + Configuration.indentation;
if (!(entity instanceof ObjectEntity)) {
return super.doWrite(
entity,
insideString,
indentation,
wrap,
attributeSeparator,
trailingSeparator,
attributeValueConjunctionSign,
// @ts-expect-error
key => entity[key] instanceof ObjectEntity ? "" : attributeKeyPrinter(key)
)
}
let result = indentation + "Begin Object"
+ (entity.Class?.type || entity.Class?.path ? ` Class=${this.doWriteValue(entity.Class, insideString)}` : "")
+ (entity.Name ? ` Name=${this.doWriteValue(entity.Name, insideString)}` : "")
+ (entity.Archetype ? ` Archetype=${this.doWriteValue(entity.Archetype, insideString)}` : "")
+ (entity.ExportPath?.type || entity.ExportPath?.path ? ` ExportPath=${this.doWriteValue(entity.ExportPath, insideString)}` : "")
+ "\n"
+ super.doWrite(
entity,
insideString,
moreIndentation,
wrap,
attributeSeparator,
true,
attributeValueConjunctionSign,
key => entity[key] instanceof ObjectEntity ? "" : attributeKeyPrinter(key)
)
+ (!AttributeInfo.getAttribute(entity, "CustomProperties", "ignored")
? entity.getCustomproperties().map(pin =>
moreIndentation
+ attributeKeyPrinter("CustomProperties ")
+ SerializerFactory.getSerializer(PinEntity).doWrite(pin, insideString)
+ this.attributeSeparator
).join("")
: ""
)
+ indentation + "End Object";
return result
}
}
/**
* @typedef {import("../IInput.js").Options & {
* listenOnFocus?: Boolean,
* unlistenOnTextEdit?: Boolean,
* }} Options
*/
class Copy extends IInput {
static #serializer = new ObjectSerializer()
/** @type {(e: ClipboardEvent) => void} */
#copyHandler
constructor(target, blueprint, options = {}) {
options.listenOnFocus ??= true;
options.unlistenOnTextEdit ??= true; // No nodes copy if inside a text field, just text (default behavior)
super(target, blueprint, options);
let self = this;
this.#copyHandler = () => self.copied();
}
listenEvents() {
window.addEventListener("copy", this.#copyHandler);
}
unlistenEvents() {
window.removeEventListener("copy", this.#copyHandler);
}
getSerializedText() {
return this.blueprint
.getNodes(true)
.map(node => Copy.#serializer.write(node.entity, false))
.join("")
}
copied() {
const value = this.getSerializedText();
navigator.clipboard.writeText(value);
return value
}
}
/**
* @typedef {import("../IInput.js").Options & {
* listenOnFocus?: Boolean,
* unlistenOnTextEdit?: Boolean,
* }} Options
*/
class Cut extends IInput {
static #serializer = new ObjectSerializer()
/** @type {(e: ClipboardEvent) => void} */
#cutHandler
/**
* @param {Element} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}) {
options.listenOnFocus ??= true;
options.unlistenOnTextEdit ??= true; // No nodes copy if inside a text field, just text (default behavior)
super(target, blueprint, options);
let self = this;
this.#cutHandler = () => self.cut();
}
listenEvents() {
window.addEventListener("cut", this.#cutHandler);
}
unlistenEvents() {
window.removeEventListener("cut", this.#cutHandler);
}
getSerializedText() {
return this.blueprint
.getNodes(true)
.map(node => Cut.#serializer.write(node.entity, false))
.join("")
}
cut() {
this.blueprint.template.getCopyInputObject().copied();
this.blueprint.removeGraphElement(...this.blueprint.getNodes(true));
}
}
class ElementFactory {
/** @type {Map>} */
static #elementConstructors = new Map()
/**
* @param {String} tagName
* @param {AnyConstructor} entityConstructor
*/
static registerElement(tagName, entityConstructor) {
ElementFactory.#elementConstructors.set(tagName, entityConstructor);
}
/** @param {String} tagName */
static getConstructor(tagName) {
return ElementFactory.#elementConstructors.get(tagName)
}
}
/**
* @typedef {import("../IInput.js").Options & {
* listenOnFocus?: Boolean,
* unlistenOnTextEdit?: Boolean,
* }} Options
*/
class Paste extends IInput {
static #serializer = new ObjectSerializer()
/** @type {(e: ClipboardEvent) => void} */
#pasteHandle
/**
* @param {Element} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}) {
options.listenOnFocus ??= true;
options.unlistenOnTextEdit ??= true; // No nodes paste if inside a text field, just text (default behavior)
super(target, blueprint, options);
let self = this;
this.#pasteHandle = e => self.pasted(e.clipboardData.getData("Text"));
}
listenEvents() {
window.addEventListener("paste", this.#pasteHandle);
}
unlistenEvents() {
window.removeEventListener("paste", this.#pasteHandle);
}
/** @param {String} value */
pasted(value) {
let top = 0;
let left = 0;
let count = 0;
let nodes = Paste.#serializer.readMultiple(value).map(entity => {
let node = /** @type {NodeElementConstructor} */(ElementFactory.getConstructor("ueb-node"))
.newObject(entity);
top += node.locationY;
left += node.locationX;
++count;
return node
});
top /= count;
left /= count;
if (nodes.length > 0) {
this.blueprint.unselectAll();
}
let mousePosition = this.blueprint.mousePosition;
nodes.forEach(node => {
node.addLocation(mousePosition[0] - left, mousePosition[1] - top);
node.snapToGrid();
node.setSelected(true);
});
this.blueprint.addGraphElement(...nodes);
return true
}
}
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("./IPointing.js").Options & {
* listenOnFocus?: Boolean,
* strictTarget?: Boolean,
* }} Options
*/
class MouseWheel extends IPointing {
/** @param {MouseWheel} self */
static #ignoreEvent = self => { }
#variation = 0
get variation() {
return this.#variation
}
/** @param {WheelEvent} e */
#mouseWheelHandler = e => {
if (this.enablerKey && !this.enablerActivated) {
return
}
e.preventDefault();
this.#variation = e.deltaY;
this.setLocationFromEvent(e);
this.wheel();
}
/** @param {WheelEvent} e */
#mouseParentWheelHandler = e => e.preventDefault()
/**
* @param {HTMLElement} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(
target,
blueprint,
options = {},
onWheel = MouseWheel.#ignoreEvent,
) {
options.listenOnFocus = true;
options.strictTarget ??= false;
super(target, blueprint, options);
this.strictTarget = options.strictTarget;
this.onWheel = onWheel;
}
listenEvents() {
this.movementSpace.addEventListener("wheel", this.#mouseWheelHandler, false);
this.movementSpace.parentElement?.addEventListener("wheel", this.#mouseParentWheelHandler);
}
unlistenEvents() {
this.movementSpace.removeEventListener("wheel", this.#mouseWheelHandler, false);
this.movementSpace.parentElement?.removeEventListener("wheel", this.#mouseParentWheelHandler);
}
/* Subclasses can override */
wheel() {
this.onWheel(this);
}
}
class Zoom extends MouseWheel {
#accumulatedVariation = 0
#enableZoonIn = false
get enableZoonIn() {
return this.#enableZoonIn
}
set enableZoonIn(value) {
if (value == this.#enableZoonIn) {
return
}
this.#enableZoonIn = value;
}
wheel() {
this.#accumulatedVariation += -this.variation;
if (Math.abs(this.#accumulatedVariation) < Configuration.mouseWheelZoomThreshold) {
return
}
let zoomLevel = this.blueprint.getZoom();
if (!this.enableZoonIn && zoomLevel == 0 && this.#accumulatedVariation > 0) {
return
}
zoomLevel += Math.sign(this.#accumulatedVariation);
this.blueprint.setZoom(zoomLevel, this.location);
this.#accumulatedVariation = 0;
}
}
/**
* @typedef {import("./KeyboardShortcut.js").Options & {
* activationKeys?: String | KeyBindingEntity | (String | KeyBindingEntity)[],
* }} Options
*/
class KeyboardEnableZoom extends KeyboardShortcut {
/** @type {Zoom} */
#zoomInputObject
/**
* @param {HTMLElement} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}) {
options.activationKeys = Shortcuts.enableZoomIn;
super(target, blueprint, options);
}
fire() {
this.#zoomInputObject = this.blueprint.template.getZoomInputObject();
this.#zoomInputObject.enableZoonIn = true;
}
unfire() {
this.#zoomInputObject.enableZoonIn = false;
}
}
/**
* @template {IEntity} EntityT
* @template {ITemplate} TemplateT
*/
class IElement extends s {
/** @type {Blueprint} */
#blueprint
get blueprint() {
return this.#blueprint
}
set blueprint(v) {
this.#blueprint = v;
}
/** @type {EntityT} */
#entity
get entity() {
return this.#entity
}
set entity(entity) {
this.#entity = entity;
}
/** @type {TemplateT} */
#template
get template() {
return this.#template
}
isInitialized = false
isSetup = false
/** @type {IInput[]} */
inputObjects = []
/**
* @param {EntityT} entity
* @param {TemplateT} template
*/
initialize(entity, template) {
this.requestUpdate();
this.#entity = entity;
this.#template = template;
this.#template.initialize(this);
if (this.isConnected) {
this.updateComplete.then(() => this.setup());
}
this.isInitialized = true;
}
connectedCallback() {
super.connectedCallback();
this.blueprint = /** @type {Blueprint} */(this.closest("ueb-blueprint"));
if (this.isInitialized) {
this.requestUpdate();
this.updateComplete.then(() => this.setup());
}
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.isSetup) {
this.updateComplete.then(() => this.cleanup());
}
this.acknowledgeDelete();
}
createRenderRoot() {
return this
}
setup() {
this.template.setup();
this.isSetup = true;
}
cleanup() {
this.template.cleanup();
this.isSetup = false;
}
/** @param {PropertyValues} changedProperties */
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
this.template.willUpdate(changedProperties);
}
/** @param {PropertyValues} changedProperties */
update(changedProperties) {
super.update(changedProperties);
this.template.update(changedProperties);
}
render() {
return this.template.render()
}
/** @param {PropertyValues} changedProperties */
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.template.firstUpdated(changedProperties);
this.template.inputSetup();
}
/** @param {PropertyValues} changedProperties */
updated(changedProperties) {
super.updated(changedProperties);
this.template.updated(changedProperties);
}
acknowledgeDelete() {
let deleteEvent = new CustomEvent(Configuration.removeEventName);
this.dispatchEvent(deleteEvent);
}
/** @param {IElement} element */
isSameGraph(element) {
return this.blueprint && this.blueprint == element?.blueprint
}
}
/**
* @template {IEntity} T
* @template {IDraggableTemplate} U
* @extends {IElement}
*/
class IDraggableElement extends IElement {
static properties = {
...super.properties,
locationX: {
type: Number,
attribute: false,
},
locationY: {
type: Number,
attribute: false,
},
sizeX: {
type: Number,
attribute: false,
},
sizeY: {
type: Number,
attribute: false,
},
}
static dragEventName = Configuration.dragEventName
static dragGeneralEventName = Configuration.dragGeneralEventName
constructor() {
super();
this.locationX = 0;
this.locationY = 0;
this.sizeX = 0;
this.sizeY = 0;
}
computeSizes() {
const bounding = this.getBoundingClientRect();
this.sizeX = this.blueprint.scaleCorrect(bounding.width);
this.sizeY = this.blueprint.scaleCorrect(bounding.height);
}
/** @param {PropertyValues} changedProperties */
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.computeSizes();
}
/**
* @param {Number} x
* @param {Number} y
*/
setLocation(x, y, acknowledge = true) {
const dx = x - this.locationX;
const dy = y - this.locationY;
this.locationX = x;
this.locationY = y;
if (this.blueprint && acknowledge) {
const dragLocalEvent = new CustomEvent(
/** @type {typeof IDraggableElement} */(this.constructor).dragEventName,
{
detail: {
value: [dx, dy],
},
bubbles: false,
cancelable: true,
}
);
this.dispatchEvent(dragLocalEvent);
}
}
/**
* @param {Number} x
* @param {Number} y
*/
addLocation(x, y, acknowledge = true) {
this.setLocation(this.locationX + x, this.locationY + y, acknowledge);
}
/** @param {Coordinates} value */
acknowledgeDrag(value) {
const dragEvent = new CustomEvent(
/** @type {typeof IDraggableElement} */(this.constructor).dragGeneralEventName,
{
detail: {
value: value
},
bubbles: true,
cancelable: true
}
);
this.dispatchEvent(dragEvent);
}
snapToGrid() {
const snappedLocation = Utility.snapToGrid(this.locationX, this.locationY, Configuration.gridSize);
if (this.locationX != snappedLocation[0] || this.locationY != snappedLocation[1]) {
this.setLocation(snappedLocation[0], snappedLocation[1]);
}
}
topBoundary(justSelectableArea = false) {
return this.template.topBoundary(justSelectableArea)
}
rightBoundary(justSelectableArea = false) {
return this.template.rightBoundary(justSelectableArea)
}
bottomBoundary(justSelectableArea = false) {
return this.template.bottomBoundary(justSelectableArea)
}
leftBoundary(justSelectableArea = false) {
return this.template.leftBoundary(justSelectableArea)
}
}
/**
* @typedef {import("./IPointing.js").Options & {
* clickButton?: Number,
* consumeEvent?: Boolean,
* draggableElement?: HTMLElement,
* exitAnyButton?: Boolean,
* moveEverywhere?: Boolean,
* movementSpace?: HTMLElement,
* repositionOnClick?: Boolean,
* scrollGraphEdge?: Boolean,
* strictTarget?: Boolean,
* stepSize?: Number,
* }} Options
*/
/**
* @template {IElement} T
* @extends {IPointing}
*/
class IMouseClickDrag extends IPointing {
/** @param {MouseEvent} e */
#mouseDownHandler = e => {
this.blueprint.setFocused(true);
switch (e.button) {
case this.options.clickButton:
// Either doesn't matter or consider the click only when clicking on the parent, not descandants
if (!this.options.strictTarget || e.target == e.currentTarget) {
if (this.consumeEvent) {
e.stopImmediatePropagation(); // Captured, don't call anyone else
}
// Attach the listeners
this.#movementListenedElement.addEventListener("mousemove", this.#mouseStartedMovingHandler);
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];
if (this.target instanceof IDraggableElement) {
this.clickedOffset = [
this.clickedPosition[0] - this.target.locationX,
this.clickedPosition[1] - this.target.locationY,
];
}
this.clicked(this.clickedPosition);
}
break
default:
if (!this.options.exitAnyButton) {
this.#mouseUpHandler(e);
}
break
}
}
/** @param {MouseEvent} e */
#mouseStartedMovingHandler = e => {
if (this.consumeEvent) {
e.stopImmediatePropagation(); // Captured, don't call anyone else
}
// Delegate from now on to this.#mouseMoveHandler
this.#movementListenedElement.removeEventListener("mousemove", this.#mouseStartedMovingHandler);
this.#movementListenedElement.addEventListener("mousemove", this.#mouseMoveHandler);
// Handler calls e.preventDefault() when it receives the event, this means dispatchEvent returns false
const dragEvent = this.getEvent(Configuration.trackingMouseEventName.begin);
this.#trackingMouse = this.target.dispatchEvent(dragEvent) == false;
this.setLocationFromEvent(e);
// Do actual actions
this.lastLocation = Utility.snapToGrid(this.clickedPosition[0], this.clickedPosition[1], this.stepSize);
this.startDrag(this.location);
this.started = true;
this.#mouseMoveHandler(e);
}
/** @param {MouseEvent} e */
#mouseMoveHandler = e => {
if (this.consumeEvent) {
e.stopImmediatePropagation(); // Captured, don't call anyone else
}
const location = this.setLocationFromEvent(e);
const movement = [e.movementX, e.movementY];
this.dragTo(location, movement);
if (this.#trackingMouse) {
this.blueprint.mousePosition = location;
}
if (this.options.scrollGraphEdge) {
const movementNorm = Math.sqrt(movement[0] * movement[0] + movement[1] * movement[1]);
const threshold = this.blueprint.scaleCorrect(Configuration.edgeScrollThreshold);
const leftThreshold = this.blueprint.template.gridLeftVisibilityBoundary() + threshold;
const rightThreshold = this.blueprint.template.gridRightVisibilityBoundary() - threshold;
let scrollX = 0;
if (location[0] < leftThreshold) {
scrollX = location[0] - leftThreshold;
} else if (location[0] > rightThreshold) {
scrollX = location[0] - rightThreshold;
}
const topThreshold = this.blueprint.template.gridTopVisibilityBoundary() + threshold;
const bottomThreshold = this.blueprint.template.gridBottomVisibilityBoundary() - threshold;
let scrollY = 0;
if (location[1] < topThreshold) {
scrollY = location[1] - topThreshold;
} else if (location[1] > bottomThreshold) {
scrollY = location[1] - bottomThreshold;
}
scrollX = Utility.clamp(this.blueprint.scaleCorrectReverse(scrollX) ** 3 * movementNorm * 0.6, -20, 20);
scrollY = Utility.clamp(this.blueprint.scaleCorrectReverse(scrollY) ** 3 * movementNorm * 0.6, -20, 20);
this.blueprint.scrollDelta(scrollX, scrollY);
}
}
/** @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"
this.#movementListenedElement.removeEventListener("mousemove", this.#mouseStartedMovingHandler);
this.#movementListenedElement.removeEventListener("mousemove", this.#mouseMoveHandler);
document.removeEventListener("mouseup", this.#mouseUpHandler);
if (this.started) {
this.endDrag();
}
this.unclicked();
if (this.#trackingMouse) {
const dragEvent = this.getEvent(Configuration.trackingMouseEventName.end);
this.target.dispatchEvent(dragEvent);
this.#trackingMouse = false;
}
this.started = false;
}
}
#trackingMouse = false
#movementListenedElement
#draggableElement
get draggableElement() {
return this.#draggableElement
}
clickedOffset = /** @type {Coordinates} */([0, 0])
clickedPosition = /** @type {Coordinates} */([0, 0])
lastLocation = /** @type {Coordinates} */([0, 0])
started = false
stepSize = 1
/**
* @param {T} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}) {
options.clickButton ??= Configuration.mouseClickButton;
options.consumeEvent ??= true;
options.draggableElement ??= target;
options.exitAnyButton ??= true;
options.moveEverywhere ??= false;
options.movementSpace ??= blueprint?.getGridDOMElement();
options.repositionOnClick ??= false;
options.scrollGraphEdge ??= false;
options.strictTarget ??= false;
super(target, blueprint, options);
this.stepSize = Number(options.stepSize ?? Configuration.gridSize);
this.#movementListenedElement = this.options.moveEverywhere ? document.documentElement : this.movementSpace;
this.#draggableElement = /** @type {HTMLElement} */(this.options.draggableElement);
this.listenEvents();
}
listenEvents() {
super.listenEvents();
this.#draggableElement.addEventListener("mousedown", this.#mouseDownHandler);
if (this.options.clickButton === Configuration.mouseRightClickButton) {
this.#draggableElement.addEventListener("contextmenu", e => e.preventDefault());
}
}
unlistenEvents() {
super.unlistenEvents();
this.#draggableElement.removeEventListener("mousedown", this.#mouseDownHandler);
}
getEvent(eventName) {
return new CustomEvent(eventName, {
detail: {
tracker: this
},
bubbles: true,
cancelable: true
})
}
/* Subclasses will override the following methods */
clicked(location) {
}
startDrag(location) {
}
dragTo(location, offset) {
}
endDrag() {
}
unclicked(location) {
}
}
class MouseScrollGraph extends IMouseClickDrag {
startDrag() {
this.blueprint.scrolling = true;
}
/**
* @param {Coordinates} location
* @param {Coordinates} movement
*/
dragTo(location, movement) {
this.blueprint.scrollDelta(-movement[0], -movement[1]);
}
endDrag() {
this.blueprint.scrolling = false;
}
}
/**
* @typedef {import("./IPointing.js").Options & {
* listenOnFocus?: Boolean,
* }} Options
*/
class MouseTracking extends IPointing {
/** @type {IPointing} */
#mouseTracker = null
/** @param {MouseEvent} e */
#mousemoveHandler = e => {
e.preventDefault();
this.setLocationFromEvent(e);
this.blueprint.mousePosition = [...this.location];
}
/** @param {CustomEvent} e */
#trackingMouseStolenHandler = e => {
if (!this.#mouseTracker) {
e.preventDefault();
this.#mouseTracker = e.detail.tracker;
this.unlistenMouseMove();
}
}
/** @param {CustomEvent} e */
#trackingMouseGaveBackHandler = e => {
if (this.#mouseTracker == e.detail.tracker) {
e.preventDefault();
this.#mouseTracker = null;
this.listenMouseMove();
}
}
/**
* @param {Element} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}) {
options.listenOnFocus = true;
super(target, blueprint, options);
}
listenMouseMove() {
this.target.addEventListener("mousemove", this.#mousemoveHandler);
}
unlistenMouseMove() {
this.target.removeEventListener("mousemove", this.#mousemoveHandler);
}
listenEvents() {
this.listenMouseMove();
this.blueprint.addEventListener(
Configuration.trackingMouseEventName.begin,
/** @type {(e: Event) => any} */(this.#trackingMouseStolenHandler));
this.blueprint.addEventListener(
Configuration.trackingMouseEventName.end,
/** @type {(e: Event) => any} */(this.#trackingMouseGaveBackHandler));
}
unlistenEvents() {
this.unlistenMouseMove();
this.blueprint.removeEventListener(
Configuration.trackingMouseEventName.begin,
/** @type {(e: Event) => any} */(this.#trackingMouseStolenHandler));
this.blueprint.removeEventListener(
Configuration.trackingMouseEventName.end,
/** @type {(e: Event) => any} */(this.#trackingMouseGaveBackHandler)
);
}
}
/**
* @typedef {import("./IMouseClickDrag.js").Options & {
* scrollGraphEdge?: Boolean,
* }} Options
*/
class Select extends IMouseClickDrag {
constructor(target, blueprint, options = {}) {
options.scrollGraphEdge ??= true;
super(target, blueprint, options);
this.selectorElement = this.blueprint.template.selectorElement;
}
startDrag() {
this.selectorElement.beginSelect(this.clickedPosition);
}
/**
* @param {Coordinates} location
* @param {Coordinates} movement
*/
dragTo(location, movement) {
this.selectorElement.selectTo(location);
}
endDrag() {
if (this.started) {
this.selectorElement.endSelect();
}
}
unclicked() {
if (!this.started) {
this.blueprint.unselectAll();
}
}
}
/**
* @typedef {import("../IInput.js").Options & {
* listenOnFocus?: Boolean,
* }} Options
*/
class Unfocus extends IInput {
/** @param {MouseEvent} e */
#clickHandler = e => this.clickedSomewhere(/** @type {HTMLElement} */(e.target))
/**
* @param {HTMLElement} target
* @param {Blueprint} blueprint
* @param {Options} options
*/
constructor(target, blueprint, options = {}) {
options.listenOnFocus = true;
super(target, blueprint, options);
if (this.blueprint.focus) {
document.addEventListener("click", this.#clickHandler);
}
}
/** @param {HTMLElement} target */
clickedSomewhere(target) {
// If target is outside the blueprint grid
if (!target.closest("ueb-blueprint")) {
this.blueprint.setFocused(false);
}
}
listenEvents() {
document.addEventListener("click", this.#clickHandler);
}
unlistenEvents() {
document.removeEventListener("click", this.#clickHandler);
}
}
/** @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();
}
}
/** @extends ITemplate */
class BlueprintTemplate extends ITemplate {
static styleVariables = {
"--ueb-font-size": `${Configuration.fontSize}`,
"--ueb-grid-axis-line-color": `${Configuration.gridAxisLineColor}`,
"--ueb-grid-expand": `${Configuration.expandGridSize}px`,
"--ueb-grid-line-color": `${Configuration.gridLineColor}`,
"--ueb-grid-line-width": `${Configuration.gridLineWidth}px`,
"--ueb-grid-set-line-color": `${Configuration.gridSetLineColor}`,
"--ueb-grid-set": `${Configuration.gridSet}`,
"--ueb-grid-size": `${Configuration.gridSize}px`,
"--ueb-link-min-width": `${Configuration.linkMinWidth}`,
"--ueb-node-radius": `${Configuration.nodeRadius}px`,
}
#resizeObserver = new ResizeObserver(entries => {
const size = entries.find(entry => entry.target === this.viewportElement)?.devicePixelContentBoxSize?.[0];
if (size) {
this.viewportSize[0] = size.inlineSize;
this.viewportSize[1] = size.blockSize;
}
})
/** @type {Copy} */
#copyInputObject
/** @type {Paste} */
#pasteInputObject
/** @type {Zoom} */
#zoomInputObject
/** @type {HTMLElement} */ headerElement
/** @type {HTMLElement} */ overlayElement
/** @type {HTMLElement} */ viewportElement
/** @type {SelectorElement} */ selectorElement
/** @type {HTMLElement} */ gridElement
/** @type {HTMLElement} */ linksContainerElement
/** @type {HTMLElement} */ nodesContainerElement
viewportSize = [0, 0]
#setViewportSize() {
}
/** @param {Blueprint} element */
initialize(element) {
super.initialize(element);
this.element.style.cssText = Object.entries(BlueprintTemplate.styleVariables)
.map(([k, v]) => `${k}:${v};`).join("");
const htmlTemplate = /** @type {HTMLTemplateElement} */(
this.element.querySelector(":scope > template")
)?.content.textContent;
if (htmlTemplate) {
this.element.requestUpdate();
this.element.updateComplete.then(() => {
this.blueprint.mousePosition = [
Math.round(this.viewportSize[0] / 2),
Math.round(this.viewportSize[1] / 2),
];
this.getPasteInputObject().pasted(htmlTemplate);
this.blueprint.unselectAll();
});
}
}
setup() {
super.setup();
this.#resizeObserver.observe(this.viewportElement, {
box: "device-pixel-content-box",
});
const bounding = this.viewportElement.getBoundingClientRect();
this.viewportSize[0] = bounding.width;
this.viewportSize[1] = bounding.height;
if (this.blueprint.nodes.length > 0) {
this.blueprint.requestUpdate();
this.blueprint.updateComplete.then(() => this.centerContentInViewport());
}
}
cleanup() {
super.cleanup();
this.#resizeObserver.unobserve(this.viewportElement);
}
createInputObjects() {
const gridElement = this.element.getGridDOMElement();
this.#copyInputObject = new Copy(gridElement, this.blueprint);
this.#pasteInputObject = new Paste(gridElement, this.blueprint);
this.#zoomInputObject = new Zoom(gridElement, this.blueprint);
return [
...super.createInputObjects(),
this.#copyInputObject,
this.#pasteInputObject,
this.#zoomInputObject,
new Cut(gridElement, this.blueprint),
new KeyboardShortcut(gridElement, this.blueprint, {
activationKeys: Shortcuts.duplicateNodes
}, () =>
this.blueprint.template.getPasteInputObject().pasted(
this.blueprint.template.getCopyInputObject().copied()
)
),
new KeyboardShortcut(gridElement, this.blueprint, {
activationKeys: Shortcuts.deleteNodes
}, () => this.blueprint.removeGraphElement(...this.blueprint.getNodes(true))),
new KeyboardShortcut(gridElement, this.blueprint, {
activationKeys: Shortcuts.selectAllNodes
}, () => this.blueprint.selectAll()),
new Select(gridElement, this.blueprint, {
clickButton: Configuration.mouseClickButton,
exitAnyButton: true,
moveEverywhere: true,
}),
new MouseScrollGraph(gridElement, this.blueprint, {
clickButton: Configuration.mouseRightClickButton,
exitAnyButton: false,
moveEverywhere: true,
}),
new Unfocus(gridElement, this.blueprint),
new MouseTracking(gridElement, this.blueprint),
new KeyboardEnableZoom(gridElement, this.blueprint),
]
}
render() {
return x`
`
}
/** @param {PropertyValues} changedProperties */
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.headerElement = this.blueprint.querySelector('.ueb-viewport-header');
this.overlayElement = this.blueprint.querySelector('.ueb-viewport-overlay');
this.viewportElement = this.blueprint.querySelector('.ueb-viewport-body');
this.selectorElement = this.blueprint.querySelector('ueb-selector');
this.gridElement = this.viewportElement.querySelector(".ueb-grid");
this.linksContainerElement = this.blueprint.querySelector("[data-links]");
this.linksContainerElement.append(...this.blueprint.getLinks());
this.nodesContainerElement = this.blueprint.querySelector("[data-nodes]");
this.nodesContainerElement.append(...this.blueprint.getNodes());
this.viewportElement.scroll(Configuration.expandGridSize, Configuration.expandGridSize);
}
/** @param {PropertyValues} changedProperties */
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
if (this.headerElement && changedProperties.has("zoom")) {
this.headerElement.classList.add("ueb-zoom-changed");
this.headerElement.addEventListener(
"animationend",
() => this.headerElement.classList.remove("ueb-zoom-changed")
);
}
}
/** @param {PropertyValues} changedProperties */
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has("scrollX") || changedProperties.has("scrollY")) {
this.viewportElement.scroll(this.blueprint.scrollX, this.blueprint.scrollY);
}
if (changedProperties.has("zoom")) {
this.blueprint.style.setProperty("--ueb-scale", this.blueprint.getScale());
const previousZoom = changedProperties.get("zoom");
const minZoom = Math.min(previousZoom, this.blueprint.zoom);
const maxZoom = Math.max(previousZoom, this.blueprint.zoom);
const classes = Utility.range(minZoom, maxZoom);
const getClassName = v => `ueb-zoom-${v}`;
if (previousZoom < this.blueprint.zoom) {
this.blueprint.classList.remove(...classes.filter(v => v < 0).map(getClassName));
this.blueprint.classList.add(...classes.filter(v => v > 0).map(getClassName));
} else {
this.blueprint.classList.remove(...classes.filter(v => v > 0).map(getClassName));
this.blueprint.classList.add(...classes.filter(v => v < 0).map(getClassName));
}
}
}
getCommentNodes(justSelected = false) {
return this.blueprint.querySelectorAll(
`ueb-node[data-type="${Configuration.paths.comment}"]${justSelected ? '[data-selected="true"]' : ''}`
+ `, ueb-node[data-type="${Configuration.paths.materialGraphNodeComment}"]${justSelected ? '[data-selected="true"]' : ''}`
)
}
/** @param {PinReferenceEntity} pinReference */
getPin(pinReference) {
return /** @type {PinElement} */(this.blueprint.querySelector(
`ueb-node[data-title="${pinReference.objectName}"] ueb-pin[data-id="${pinReference.pinGuid}"]`
))
}
getCopyInputObject() {
return this.#copyInputObject
}
getPasteInputObject() {
return this.#pasteInputObject
}
getZoomInputObject() {
return this.#zoomInputObject
}
/**
* @param {Number} x
* @param {Number} y
*/
isPointVisible(x, y) {
return false
}
gridTopVisibilityBoundary() {
return this.blueprint.scaleCorrect(this.blueprint.scrollY) - this.blueprint.translateY
}
gridRightVisibilityBoundary() {
return this.gridLeftVisibilityBoundary() + this.blueprint.scaleCorrect(this.viewportSize[0])
}
gridBottomVisibilityBoundary() {
return this.gridTopVisibilityBoundary() + this.blueprint.scaleCorrect(this.viewportSize[1])
}
gridLeftVisibilityBoundary() {
return this.blueprint.scaleCorrect(this.blueprint.scrollX) - this.blueprint.translateX
}
centerViewport(x = 0, y = 0, smooth = true) {
const centerX = this.gridLeftVisibilityBoundary() + this.blueprint.scaleCorrect(this.viewportSize[0] / 2);
const centerY = this.gridTopVisibilityBoundary() + this.blueprint.scaleCorrect(this.viewportSize[1] / 2);
this.blueprint.scrollDelta(
this.blueprint.scaleCorrectReverse(x - centerX),
this.blueprint.scaleCorrectReverse(y - centerY),
smooth
);
}
centerContentInViewport(smooth = true) {
let avgX = 0;
let avgY = 0;
let minX = Number.MAX_SAFE_INTEGER;
let maxX = Number.MIN_SAFE_INTEGER;
let minY = Number.MAX_SAFE_INTEGER;
let maxY = Number.MIN_SAFE_INTEGER;
const nodes = this.blueprint.getNodes();
for (const node of nodes) {
avgX += node.leftBoundary() + node.rightBoundary();
avgY += node.topBoundary() + node.bottomBoundary();
minX = Math.min(minX, node.leftBoundary());
maxX = Math.max(maxX, node.rightBoundary());
minY = Math.min(minY, node.topBoundary());
maxY = Math.max(maxY, node.bottomBoundary());
}
avgX = Math.round(maxX - minX <= this.viewportSize[0]
? (maxX + minX) / 2
: avgX / (2 * nodes.length)
);
avgY = Math.round(maxY - minY <= this.viewportSize[1]
? (maxY + minY) / 2
: avgY / (2 * nodes.length)
);
this.centerViewport(avgX, avgY, smooth);
}
}
/**
* @template {IEntity} EntityT
* @template {ITemplate} TemplateT
* @extends {IElement}
*/
class IFromToPositionedElement extends IElement {
static properties = {
...super.properties,
fromX: {
type: Number,
attribute: false,
},
fromY: {
type: Number,
attribute: false,
},
toX: {
type: Number,
attribute: false,
},
toY: {
type: Number,
attribute: false,
},
}
constructor() {
super();
this.fromX = 0;
this.fromY = 0;
this.toX = 0;
this.toY = 0;
}
/** @param {Coordinates} param0 */
setBothLocations([x, y]) {
this.fromX = x;
this.fromY = y;
this.toX = x;
this.toY = y;
}
/**
* @param {Number} x
* @param {Number} y
*/
addSourceLocation(x, y) {
this.fromX += x;
this.fromY += y;
}
/**
* @param {Number} x
* @param {Number} y
*/
addDestinationLocation(x, y) {
this.toX += x;
this.toY += y;
}
}
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 {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 {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