Files
ueblueprint/js/element/NodeElement.js
2022-12-28 19:02:29 +01:00

347 lines
13 KiB
JavaScript

import CommentNodeTemplate from "../template/node/CommentNodeTemplate"
import Configuration from "../Configuration"
import IdentifierEntity from "../entity/IdentifierEntity"
import ISelectableDraggableElement from "./ISelectableDraggableElement"
import KnotNodeTemplate from "../template/node/KnotNodeTemplate"
import NodeTemplate from "../template/node/NodeTemplate"
import ObjectEntity from "../entity/ObjectEntity"
import PinEntity from "../entity/PinEntity"
import PinReferenceEntity from "../entity/PinReferenceEntity"
import SerializerFactory from "../serialization/SerializerFactory"
import Utility from "../Utility"
import VariableAccessNodeTemplate from "../template/node/VariableAccessNodeTemplate"
import VariableConversionNodeTemplate from "../template/node/VariableConversionNodeTemplate"
import VariableOperationNodeTemplate from "../template/node/VariableOperationNodeTemplate"
/**
* @typedef {import("./IElement").default} IElement
* @typedef {import("./PinElement").default} PinElement
* @typedef {typeof NodeElement} NodeElementConstructor
*/
/** @extends {ISelectableDraggableElement<ObjectEntity, NodeTemplate>} */
export default class NodeElement extends ISelectableDraggableElement {
static #typeTemplateMap = {
}
static properties = {
...ISelectableDraggableElement.properties,
typePath: {
type: String,
attribute: "data-type",
reflect: true,
},
nodeName: {
type: String,
attribute: "data-name",
reflect: true,
},
advancedPinDisplay: {
type: String,
attribute: "data-advanced-display",
converter: IdentifierEntity.attributeConverter,
reflect: true,
},
enabledState: {
type: String,
attribute: "data-enabled-state",
reflect: true,
},
nodeDisplayName: {
type: String,
attribute: false,
},
pureFunction: {
type: Boolean,
converter: Utility.booleanConverter,
attribute: "data-pure-function",
reflect: true,
},
}
static dragEventName = Configuration.nodeDragEventName
static dragGeneralEventName = Configuration.nodeDragGeneralEventName
get blueprint() {
return super.blueprint
}
set blueprint(v) {
super.blueprint = v
this.#pins.forEach(p => p.blueprint = v)
}
/** @type {HTMLElement} */
#nodeNameElement
get nodeNameElement() {
return this.#nodeNameElement
}
set nodeNameElement(value) {
this.#nodeNameElement = value
}
/** @type {PinElement[]} */
#pins = []
/** @type {NodeElement[]} */
boundComments = []
#commentDragged = false
#commentDragHandler = e => {
// If selected, it will already drag, also must check if under nested comments, it must drag just once
if (!this.selected && !this.#commentDragged) {
this.#commentDragged = true
this.addNextUpdatedCallbacks(() => this.#commentDragged = false)
this.addLocation(e.detail.value)
}
}
/**
* @param {ObjectEntity} nodeEntity
* @return {new () => NodeTemplate}
*/
static getTypeTemplate(nodeEntity) {
if (
nodeEntity.getClass() === Configuration.nodeType.callFunction
|| nodeEntity.getClass() === Configuration.nodeType.commutativeAssociativeBinaryOperator
) {
if (nodeEntity.FunctionReference.MemberParent.path === "/Script/Engine.KismetMathLibrary") {
if (nodeEntity.FunctionReference.MemberName?.startsWith("Conv_")) {
return VariableConversionNodeTemplate
}
if (nodeEntity.FunctionReference.MemberName?.startsWith("Percent_")) {
return VariableOperationNodeTemplate
}
switch (nodeEntity.FunctionReference.MemberName) {
case "Abs":
case "BMax":
case "BMin":
case "Exp":
case "FMax":
case "FMin":
case "Max":
case "MaxInt64":
case "Min":
case "MinInt64":
return VariableOperationNodeTemplate
}
}
}
switch (nodeEntity.getClass()) {
case Configuration.nodeType.comment: return CommentNodeTemplate
case Configuration.nodeType.knot: return KnotNodeTemplate
case Configuration.nodeType.variableGet: return VariableAccessNodeTemplate
case Configuration.nodeType.variableSet: return VariableAccessNodeTemplate
}
return NodeTemplate
}
/** @param {String} str */
static fromSerializedObject(str) {
str = str.trim()
let entity = SerializerFactory.getSerializer(ObjectEntity).deserialize(str)
return NodeElement.newObject(/** @type {ObjectEntity} */(entity))
}
/**
* @param {ObjectEntity} entity
* @param {NodeTemplate} template
*/
static newObject(entity = new ObjectEntity(), template = new (NodeElement.getTypeTemplate(entity))()) {
const result = new NodeElement()
result.initialize(entity, template)
return result
}
initialize(entity = new ObjectEntity(), template = new (NodeElement.getTypeTemplate(entity))()) {
super.initialize(entity, template)
this.#pins = this.template.createPinElements()
this.typePath = this.entity.getType()
this.nodeName = this.entity.getObjectName()
this.advancedPinDisplay = this.entity.AdvancedPinDisplay?.toString()
this.enabledState = this.entity.EnabledState
this.nodeDisplayName = this.getNodeDisplayName()
this.pureFunction = this.entity.bIsPureFunc
this.dragLinkObjects = []
super.setLocation([this.entity.NodePosX.value, this.entity.NodePosY.value])
this.entity.subscribe("AdvancedPinDisplay", value => this.advancedPinDisplay = value)
this.entity.subscribe("Name", value => this.nodeName = value)
if (this.entity.NodeWidth && this.entity.NodeHeight) {
this.sizeX = this.entity.NodeWidth.value
this.sizeY = this.entity.NodeHeight.value
} else {
this.updateComplete.then(() => this.computeSizes())
}
}
getUpdateComplete() {
return Promise.all([
super.getUpdateComplete(),
...this.getPinElements().map(pin => pin.updateComplete)
]).then(() => true)
}
/** @param {NodeElement} commentNode */
bindToComment(commentNode) {
if (commentNode != this && !this.boundComments.includes(commentNode)) {
commentNode.addEventListener(Configuration.nodeDragEventName, this.#commentDragHandler)
this.boundComments.push(commentNode)
}
}
/** @param {NodeElement} commentNode */
unbindFromComment(commentNode) {
const commentIndex = this.boundComments.indexOf(commentNode)
if (commentIndex >= 0) {
commentNode.removeEventListener(Configuration.nodeDragEventName, this.#commentDragHandler)
this.boundComments[commentIndex] = this.boundComments[this.boundComments.length - 1]
this.boundComments.pop()
}
}
/** @param {NodeElement} commentNode */
isInsideComment(commentNode) {
return this.topBoundary() >= commentNode.topBoundary()
&& this.rightBoundary() <= commentNode.rightBoundary()
&& this.bottomBoundary() <= commentNode.bottomBoundary()
&& this.leftBoundary() >= commentNode.leftBoundary()
}
cleanup() {
super.cleanup()
this.acknowledgeDelete()
}
getType() {
return this.entity.getType()
}
getNodeName() {
return this.entity.getObjectName()
}
getNodeDisplayName() {
switch (this.getType()) {
case Configuration.nodeType.callFunction:
case Configuration.nodeType.commutativeAssociativeBinaryOperator:
if (this.entity.FunctionReference.MemberName == "AddKey") {
let result = this.entity.FunctionReference.MemberParent.path.match(
ObjectEntity.sequencerScriptingNameRegex
)
if (result) {
return `Add Key (${Utility.formatStringName(result[1])})`
}
}
let memberName = this.entity.FunctionReference.MemberName
if (this.entity.FunctionReference.MemberParent.path == "/Script/Engine.KismetMathLibrary") {
if (memberName.startsWith("Conv_")) {
return "" // Conversio nodes do not have visible names
}
if (memberName.startsWith("Percent_")) {
return "%"
}
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 "Exp": return "e"
case "Max": return "MAX"
case "MaxInt64": return "MAX"
case "Min": return "MIN"
case "MinInt64": return "MIN"
}
}
return Utility.formatStringName(memberName)
case Configuration.nodeType.dynamicCast:
return `Cast To ${this.entity.TargetType.getName()}`
case Configuration.nodeType.executionSequence:
return "Sequence"
case Configuration.nodeType.ifThenElse:
return "Branch"
case Configuration.nodeType.forEachElementInEnum:
return `For Each ${this.entity.Enum.getName()}`
case Configuration.nodeType.forEachLoopWithBreak:
return "For Each Loop with Break"
case Configuration.nodeType.variableGet:
return ""
case Configuration.nodeType.variableSet:
return "SET"
default:
if (this.entity.getClass() === Configuration.nodeType.macro) {
return Utility.formatStringName(this.entity.MacroGraphReference.getMacroName())
} else {
return Utility.formatStringName(this.entity.getNameAndCounter()[0])
}
}
}
/** @param {Number} value */
setNodeWidth(value) {
this.entity.setNodeWidth(value)
this.sizeX = value
this.acknowledgeReflow()
}
/** @param {Number} value */
setNodeHeight(value) {
this.entity.setNodeHeight(value)
this.sizeY = value
this.acknowledgeReflow()
}
/** @param {IElement[]} nodesWhitelist */
sanitizeLinks(nodesWhitelist = []) {
this.getPinElements().forEach(pin => pin.sanitizeLinks(nodesWhitelist))
}
/** @param {String} name */
rename(name) {
if (this.entity.Name == name) {
return false
}
for (let sourcePinElement of this.getPinElements()) {
for (let targetPinReference of sourcePinElement.getLinks()) {
this.blueprint.getPin(targetPinReference).redirectLink(sourcePinElement, new PinReferenceEntity({
objectName: name,
pinGuid: sourcePinElement.entity.PinId,
}))
}
}
this.entity.Name = name
}
getPinElements() {
return this.#pins
}
/** @returns {PinEntity[]} */
getPinEntities() {
return this.entity.CustomProperties.filter(v => v instanceof PinEntity)
}
setLocation(value = [0, 0], acknowledge = true) {
this.entity.NodePosX.value = value[0]
this.entity.NodePosY.value = value[1]
super.setLocation(value, acknowledge)
}
acknowledgeDelete() {
let deleteEvent = new CustomEvent(Configuration.nodeDeleteEventName)
this.dispatchEvent(deleteEvent)
}
acknowledgeReflow() {
this.addNextUpdatedCallbacks(() => this.computeSizes(), true)
let reflowEvent = new CustomEvent(Configuration.nodeReflowEventName)
this.dispatchEvent(reflowEvent)
}
setShowAdvancedPinDisplay(value) {
this.entity.AdvancedPinDisplay = new IdentifierEntity(value ? "Shown" : "Hidden")
}
toggleShowAdvancedPinDisplay() {
this.setShowAdvancedPinDisplay(this.entity.AdvancedPinDisplay?.toString() != "Shown")
}
}