mirror of
https://github.com/barsdeveloper/ueblueprint.git
synced 2026-02-28 03:24:43 +08:00
Add pin (#6)
* Add pin and minor style fix WIP * Fix default pin graphics * Fix sorting * Min, max nodes are variadic
This commit is contained in:
@@ -157,6 +157,7 @@ export default class Configuration {
|
||||
select: "/Script/BlueprintGraph.K2Node_Select",
|
||||
spawnActorFromClass: "/Script/BlueprintGraph.K2Node_SpawnActorFromClass",
|
||||
switchEnum: "/Script/BlueprintGraph.K2Node_SwitchEnum",
|
||||
switchGameplayTag: "/Script/GameplayTagsEditor.GameplayTagsK2Node_SwitchGameplayTag",
|
||||
switchInteger: "/Script/BlueprintGraph.K2Node_SwitchInteger",
|
||||
switchName: "/Script/BlueprintGraph.K2Node_SwitchName",
|
||||
switchString: "/Script/BlueprintGraph.K2Node_SwitchString",
|
||||
|
||||
@@ -256,6 +256,12 @@ export default class SVGIcon {
|
||||
</svg>
|
||||
`
|
||||
|
||||
static plusCircle = html`
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M8.00016 10.6667V5.33334M5.3335 8H10.6668M8.00016 1.33334C4.31826 1.33334 1.3335 4.3181 1.3335 8C1.3335 11.6819 4.31826 14.6667 8.00016 14.6667C11.6821 14.6667 14.6668 11.6819 14.6668 8C14.6668 4.3181 11.6821 1.33334 8.00016 1.33334Z"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
static questionMark = html`
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 15C9.10456 15 10 14.1046 10 13C10 11.8954 9.10456 11 8 11C6.89544 11 6 11.8954 6 13C6 14.1046 6.89544 15 8 15Z" fill="white" />
|
||||
|
||||
@@ -159,11 +159,8 @@ export default class IEntity {
|
||||
assignAttribute(Utility.sanitize(value, /** @type {AnyValueConstructor<*>} */(defaultType)))
|
||||
continue // We have a value, need nothing more
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
assignAttribute(Utility.sanitize(
|
||||
/** @type {AnyValue} */(defaultValue),
|
||||
/** @type {AnyValueConstructor<AnyValue>} */(defaultType)
|
||||
))
|
||||
if (Object.hasOwn(attribute, "default")) { // Accept also explicit undefined
|
||||
assignAttribute(defaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,18 +30,30 @@ export default class ObjectEntity extends IEntity {
|
||||
ExportPath: {
|
||||
type: ObjectReferenceEntity,
|
||||
},
|
||||
PinNames: {
|
||||
type: [String],
|
||||
default: undefined, // To keep the order, may be defined in additionalPinInserter()
|
||||
inlined: true,
|
||||
},
|
||||
AxisKey: {
|
||||
type: SymbolEntity,
|
||||
},
|
||||
InputAxisKey: {
|
||||
type: SymbolEntity,
|
||||
},
|
||||
NumAdditionalInputs: {
|
||||
type: Number,
|
||||
default: undefined, // To keep the order, may be defined in additionalPinInserter()
|
||||
},
|
||||
bIsPureFunc: {
|
||||
type: Boolean,
|
||||
},
|
||||
bIsConstFunc: {
|
||||
type: Boolean,
|
||||
},
|
||||
bIsCaseSensitive: {
|
||||
type: Boolean,
|
||||
},
|
||||
VariableReference: {
|
||||
type: VariableReferenceEntity,
|
||||
},
|
||||
@@ -274,10 +286,13 @@ export default class ObjectEntity extends IEntity {
|
||||
/** @type {ObjectReferenceEntity} */ this.Class
|
||||
/** @type {String} */ this.Name
|
||||
/** @type {ObjectReferenceEntity} */ this.ExportPath
|
||||
/** @type {String[]} */ this.PinNames
|
||||
/** @type {SymbolEntity?} */ this.AxisKey
|
||||
/** @type {SymbolEntity?} */ this.InputAxisKey
|
||||
/** @type {Number?} */ this.NumAdditionalInputs
|
||||
/** @type {Boolean?} */ this.bIsPureFunc
|
||||
/** @type {Boolean?} */ this.bIsConstFunc
|
||||
/** @type {Boolean?} */ this.bIsCaseSensitive
|
||||
/** @type {VariableReferenceEntity?} */ this.VariableReference
|
||||
/** @type {SymbolEntity?} */ this.SelfContextInfo
|
||||
/** @type {String?} */ this.DelegatePropertyName
|
||||
@@ -635,11 +650,14 @@ export default class ObjectEntity extends IEntity {
|
||||
let memberName = this.FunctionReference?.MemberName
|
||||
if (memberName) {
|
||||
const memberParent = this.FunctionReference.MemberParent?.path ?? ""
|
||||
if (memberName === "AddKey") {
|
||||
let result = memberParent.match(ObjectEntity.sequencerScriptingNameRegex)
|
||||
if (result) {
|
||||
return `Add Key (${Utility.formatStringName(result[1])})`
|
||||
}
|
||||
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) {
|
||||
@@ -813,4 +831,97 @@ export default class ObjectEntity extends IEntity {
|
||||
}
|
||||
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:
|
||||
switch (this.FunctionReference?.MemberName) {
|
||||
case "BMax":
|
||||
case "BMin":
|
||||
case "Concat_StrStr":
|
||||
case "FMax":
|
||||
case "FMin":
|
||||
case "Max":
|
||||
case "MaxInt64":
|
||||
case "Min":
|
||||
case "MinInt64":
|
||||
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.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.switchName:
|
||||
case Configuration.paths.switchString:
|
||||
pinEntities ??= () => this.getPinEntities().filter(pinEntity => pinEntity.isOutput())
|
||||
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.match(/^\s*Case_(\d+)\s*$/)?.[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
|
||||
})
|
||||
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.CustomProperties.push(newPin)
|
||||
return newPin
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Utility from "../../Utility.js"
|
||||
* @typedef {import("../../element/NodeElement.js").default} NodeElement
|
||||
* @typedef {import("../../element/PinElement.js").default} PinElement
|
||||
* @typedef {import("../../element/PinElement.js").PinElementConstructor} PinElementConstructor
|
||||
* @typedef {import("../../entity/PinEntity.js").default} PinEntity
|
||||
* @typedef {import("lit").PropertyValues} PropertyValues
|
||||
*/
|
||||
|
||||
@@ -16,9 +17,33 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
|
||||
|
||||
/** @typedef {typeof NodeTemplate} NodeTemplateConstructor */
|
||||
|
||||
static nodeStyleClasses = ["ueb-node-style-default"]
|
||||
|
||||
#hasSubtitle = false
|
||||
|
||||
static nodeStyleClasses = ["ueb-node-style-default"]
|
||||
/** @type {() => PinEntity} */
|
||||
pinInserter
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
inputContainer
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
outputContainer
|
||||
|
||||
/** @type {PinElement} */
|
||||
pinElement
|
||||
|
||||
addPinHandler = () => {
|
||||
const pin = this.pinInserter?.()
|
||||
if (pin) {
|
||||
if (this.defaultPin && this.defaultPin.isInput() === pin.isInput()) {
|
||||
this.defaultPin.before(this.createPinElement(pin))
|
||||
} else {
|
||||
(pin.isInput() ? this.inputContainer : this.outputContainer).appendChild(this.createPinElement(pin))
|
||||
}
|
||||
this.element.acknowledgeReflow()
|
||||
}
|
||||
}
|
||||
|
||||
toggleAdvancedDisplayHandler = () => {
|
||||
this.element.toggleShowAdvancedPinDisplay()
|
||||
@@ -26,11 +51,23 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
|
||||
this.element.updateComplete.then(() => this.element.acknowledgeReflow())
|
||||
}
|
||||
|
||||
/** @param {PinEntity} pinEntity */
|
||||
createPinElement(pinEntity) {
|
||||
const pinElement = /** @type {PinElementConstructor} */(ElementFactory.getConstructor("ueb-pin"))
|
||||
.newObject(pinEntity, undefined, this.element)
|
||||
if (this.pinInserter && !this.defaultPin && pinElement.getPinName() === "Default") {
|
||||
this.defaultPin = pinElement
|
||||
this.defaultPin.classList.add("ueb-node-variadic-default")
|
||||
}
|
||||
return pinElement
|
||||
}
|
||||
|
||||
/** @param {NodeElement} element */
|
||||
initialize(element) {
|
||||
super.initialize(element)
|
||||
this.element.classList.add(.../** @type {NodeTemplateConstructor} */(this.constructor).nodeStyleClasses)
|
||||
this.element.style.setProperty("--ueb-node-color", this.getColor().cssText)
|
||||
this.pinInserter = this.element.entity.additionalPinInserter()
|
||||
}
|
||||
|
||||
getColor() {
|
||||
@@ -45,6 +82,11 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
|
||||
<div class="ueb-node-content">
|
||||
<div class="ueb-node-inputs"></div>
|
||||
<div class="ueb-node-outputs"></div>
|
||||
${this.pinInserter ? html`
|
||||
<div class="ueb-node-variadic" @click="${this.addPinHandler}">
|
||||
Add pin ${SVGIcon.plusCircle}
|
||||
</div>
|
||||
`: nothing}
|
||||
</div>
|
||||
${this.element.entity.isDevelopmentOnly() ? html`
|
||||
<div class="ueb-node-developmentonly">
|
||||
@@ -94,25 +136,31 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
|
||||
/** @param {PropertyValues} changedProperties */
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties)
|
||||
this.inputContainer = this.element.querySelector(".ueb-node-inputs")
|
||||
this.outputContainer = this.element.querySelector(".ueb-node-outputs")
|
||||
this.setupPins()
|
||||
this.element.updateComplete.then(() => this.element.acknowledgeReflow())
|
||||
}
|
||||
|
||||
setupPins() {
|
||||
const inputContainer = /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-inputs"))
|
||||
const outputContainer = /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-outputs"))
|
||||
this.element.nodeNameElement = /** @type {HTMLElement} */(this.element.querySelector(".ueb-node-name-text"))
|
||||
let hasInput = false
|
||||
let hasOutput = false
|
||||
this.element.getPinElements().forEach(p => {
|
||||
for (const p of this.element.getPinElements()) {
|
||||
if (p === this.defaultPin) {
|
||||
continue
|
||||
}
|
||||
if (p.isInput()) {
|
||||
inputContainer.appendChild(p)
|
||||
this.inputContainer.appendChild(p)
|
||||
hasInput = true
|
||||
} else if (p.isOutput()) {
|
||||
outputContainer.appendChild(p)
|
||||
this.outputContainer.appendChild(p)
|
||||
hasOutput = true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.defaultPin) {
|
||||
(this.defaultPin.isInput() ? this.inputContainer : this.outputContainer).appendChild(this.defaultPin)
|
||||
}
|
||||
if (hasInput) {
|
||||
this.element.classList.add("ueb-node-has-inputs")
|
||||
}
|
||||
@@ -127,9 +175,7 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
|
||||
.map(pinEntity => {
|
||||
this.#hasSubtitle = this.#hasSubtitle
|
||||
|| pinEntity.PinName === "self" && pinEntity.pinDisplayName() === "Target"
|
||||
let pinElement = /** @type {PinElementConstructor} */(ElementFactory.getConstructor("ueb-pin"))
|
||||
.newObject(pinEntity, undefined, this.element)
|
||||
return pinElement
|
||||
return this.createPinElement(pinEntity)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { html, nothing } from "lit"
|
||||
import ElementFactory from "../../element/ElementFactory.js"
|
||||
import NodeTemplate from "./NodeTemplate.js"
|
||||
import SVGIcon from "../../SVGIcon.js"
|
||||
|
||||
/**
|
||||
* @typedef {import("../../element/NodeElement.js").default} NodeElement
|
||||
@@ -41,6 +42,11 @@ export default class VariableManagementNodeTemplate extends NodeTemplate {
|
||||
${this.#hasOutput ? html`
|
||||
<div class="ueb-node-outputs"></div>
|
||||
` : nothing}
|
||||
${this.pinInserter ? html`
|
||||
<div class="ueb-node-variadic" @click="${this.addPinHandler}">
|
||||
Add pin ${SVGIcon.plusCircle}
|
||||
</div>
|
||||
`: nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,6 @@ export default class ExecPinTemplate extends PinTemplate {
|
||||
} else if (pinName === "execute" || pinName === "then") {
|
||||
return html``
|
||||
}
|
||||
return html`${this.element.getPinDisplayName()}`
|
||||
return html`<span class="ueb-pin-name ueb-ellipsis-nowrap-text">${this.element.getPinDisplayName()}</span>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class KnotPinTemplate extends MinimalPinTemplate {
|
||||
: this
|
||||
)
|
||||
.iconElement.getBoundingClientRect()
|
||||
const boundingLocation = [this.element.isInput() ? rect.left : rect.right, (rect.top + rect.bottom) / 2]
|
||||
const boundingLocation = [this.element.isInput() ? rect.left : rect.right + 1, (rect.top + rect.bottom) / 2]
|
||||
const location = Utility.convertLocation(boundingLocation, this.blueprint.template.gridElement)
|
||||
return this.blueprint.compensateTranslation(location[0], location[1])
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export default class PinTemplate extends ITemplate {
|
||||
|
||||
renderName() {
|
||||
return html`
|
||||
<span class="ueb-pin-name">${this.element.getPinDisplayName()}</span>
|
||||
<span class="ueb-pin-name ueb-ellipsis-nowrap-text">${this.element.getPinDisplayName()}</span>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export default class PinTemplate extends ITemplate {
|
||||
|
||||
getLinkLocation() {
|
||||
const rect = this.iconElement.getBoundingClientRect()
|
||||
const boundingLocation = [this.element.isInput() ? rect.left : rect.right, (rect.top + rect.bottom) / 2]
|
||||
const boundingLocation = [this.element.isInput() ? rect.left : rect.right + 1, (rect.top + rect.bottom) / 2]
|
||||
const location = Utility.convertLocation(boundingLocation, this.blueprint.template.gridElement)
|
||||
return this.blueprint.compensateTranslation(location[0], location[1])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user