Large refactoring and new nodes

* Fix node reference when changing elements

* Fix ScriptVariables parsing

* Fix invariant text and niagara types

* Niagara convert nodes

* Move node tests to own files

* More Niagara tests

* Niagara float and smaller fixes

* More Decoding

* More decoding

* WIP

* Float is real

* WIP

* More types and colors

* Test case and small polish

* WIP

* WIP

* Fix niagara script variables merging

* Fix Niagara variables

* Fixing mirrored ExportPath

* Fix Export paths name adjustments

* Simplify arc calculation

* Simplify a bit arc calculation

* source / destionation => origin / target

* Minor refactoring

* Fix switched link position

* Rename some properties for uniformity

* Fix input escape

* Simplify test

* About window

* Dialog backdrop style

* About dialog touches

* Remove dependency and minot improvement

* Light mode

* Fix link location and css small improvement

* Link direction and minor fixes

* Some minor fixes and refactoring

* Refactoring WIP

* Shorting repetitive bits

* More tests

* Simplify linking tests
This commit is contained in:
BarsDev
2025-02-07 00:36:03 +02:00
committed by GitHub
parent 876b8ce47f
commit 6ba2705386
347 changed files with 10108 additions and 6417 deletions

View File

@@ -19,11 +19,8 @@ export default 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}`,
@@ -55,6 +52,7 @@ export default class BlueprintTemplate extends ITemplate {
/** @type {HTMLElement} */ linksContainerElement
/** @type {HTMLElement} */ nodesContainerElement
viewportSize = [0, 0]
#removeZoomChanged = () => this.headerElement.classList.remove("ueb-zoom-changed")
/** @param {Blueprint} element */
initialize(element) {
@@ -139,6 +137,11 @@ export default class BlueprintTemplate extends ITemplate {
render() {
return html`
<div class="ueb-viewport-header">
<div class="ueb-viewport-about">
<a @click="${e => e.target.closest("ueb-blueprint").querySelector(".ueb-info-dialog").showModal()}">
</a>
</div>
<div class="ueb-viewport-zoom">
Zoom ${this.blueprint.zoom == 0 ? "1:1" : (this.blueprint.zoom > 0 ? "+" : "") + this.blueprint.zoom}
</div>
@@ -154,6 +157,22 @@ export default class BlueprintTemplate extends ITemplate {
</div>
</div>
</div>
<dialog class="ueb-info-dialog" @click="${e => e.target.closest(".ueb-info-dialog").close()}">
<h2>UEBlueprint</h2>
<p>A stand alone implementation of the UE's Blueprint visual language editor.</p>
<p>
Version: ${Configuration.VERSION}<br />
Author: barsdeveloper<br />
License: MIT<br />
<a target="_blank" href="https://github.com/barsdeveloper/ueblueprint">
<svg width="16" height="16" viewBox="0 0 24 24" fill="#e3b341" style="vertical-align: bottom">
<path d="M 12 0.587 L 16 8 L 24 9 L 18 15 L 20 24 L 12 19 L 4 24 L 6 15 L 0 9 L 8 8 Z M 0 0"></path>
</svg>
https://github.com/barsdeveloper/ueblueprint
</a>
</p>
</dialog>
`
}
@@ -176,11 +195,12 @@ export default class BlueprintTemplate extends ITemplate {
willUpdate(changedProperties) {
super.willUpdate(changedProperties)
if (this.headerElement && changedProperties.has("zoom")) {
if (this.headerElement.classList.contains("ueb-zoom-changed")) {
this.headerElement.classList.remove("ueb-zoom-changed")
void this.headerElement.offsetWidth // To trigger the reflow
}
this.headerElement.classList.add("ueb-zoom-changed")
this.headerElement.addEventListener(
"animationend",
() => this.headerElement.classList.remove("ueb-zoom-changed")
)
this.headerElement.addEventListener("animationend", this.#removeZoomChanged, { once: true })
}
}

0
js/template/IDraggableControlTemplate.js Normal file → Executable file
View File

0
js/template/IDraggablePositionedTemplate.js Normal file → Executable file
View File

View File

@@ -9,23 +9,23 @@ export default 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 [originX, originY, targetX, targetY] = [
Math.round(this.element.originX),
Math.round(this.element.originY),
Math.round(this.element.targetX),
Math.round(this.element.targetY),
]
const [left, top, width, height] = [
Math.min(fromX, toX),
Math.min(fromY, toY),
Math.abs(fromX - toX),
Math.abs(fromY - toY),
Math.min(originX, targetX),
Math.min(originY, targetY),
Math.abs(originX - targetX),
Math.abs(originY - targetY),
]
if (changedProperties.has("fromX") || changedProperties.has("toX")) {
if (changedProperties.has("originX") || changedProperties.has("targetX")) {
this.element.style.left = `${left}px`
this.element.style.width = `${width}px`
}
if (changedProperties.has("fromY") || changedProperties.has("toY")) {
if (changedProperties.has("originY") || changedProperties.has("targetY")) {
this.element.style.top = `${top}px`
this.element.style.height = `${height}px`
}

0
js/template/IResizeableTemplate.js Normal file → Executable file
View File

0
js/template/ITemplate.js Normal file → Executable file
View File

View File

@@ -1,7 +1,6 @@
import { html, nothing } from "lit"
import Configuration from "../Configuration.js"
import ElementFactory from "../element/ElementFactory.js"
import LinearColorEntity from "../entity/LinearColorEntity.js"
import KnotEntity from "../entity/objects/KnotEntity.js"
import KeyboardShortcut from "../input/keyboard/KeyboardShortcut.js"
import MouseClick from "../input/mouse/MouseClick.js"
@@ -33,41 +32,19 @@ export default class LinkTemplate extends IFromToPositionedTemplate {
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 clampedLine = x => Math.min(Math.max(0, x), 1)
static c1DecreasingValue = LinkTemplate.decreasingValue(-0.15, [100, 15])
static c2DecreasingValue = LinkTemplate.decreasingValue(-0.05, [500, 130])
static c2Clamped = LinkTemplate.clampedLine([0, 80], [200, 40])
static c2Clamped = x => -40 * LinkTemplate.clampedLine(x / 200) + 80
#uniqueId = `ueb-id-${Math.floor(Math.random() * 1E12)}`
/** @param {Coordinates} location */
#createKnot = location => {
const knotEntity = new KnotEntity({}, this.element.source.entity)
const knotEntity = new KnotEntity({}, this.element.origin.entity)
const knot = /** @type {NodeElementConstructor} */(ElementFactory.getConstructor("ueb-node"))
.newObject(knotEntity)
knot.setLocation(...this.blueprint.snapToGrid(...location))
@@ -75,13 +52,84 @@ export default class LinkTemplate extends IFromToPositionedTemplate {
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
this.element.origin = knotTemplate.outputPin
this.element.target = inputPin
}
/** @param {PropertyValues} changedProperties */
#calculateSVGPath(changedProperties) {
const originPin = this.element.origin
const targetPin = this.element.target
const isOriginAKnot = originPin?.isKnot()
const isTargetAKnot = targetPin?.isKnot()
const from = this.element.originX
const to = this.element.targetX
// Switch actual input/output pins if allowed and makes sense
if (isOriginAKnot && !targetPin) {
if (originPin?.isInputLoosely() && to > from + Configuration.distanceThreshold) {
this.element.origin = /** @type {KnotPinTemplate} */(originPin.template).getoppositePin()
} else if (originPin?.isOutputLoosely() && to < from - Configuration.distanceThreshold) {
this.element.origin = /** @type {KnotPinTemplate} */(originPin.template).getoppositePin()
}
}
if (isTargetAKnot && !originPin) {
if (targetPin?.isInputLoosely() && to < from - Configuration.distanceThreshold) {
this.element.target = /** @type {KnotPinTemplate} */(targetPin.template).getoppositePin()
} else if (targetPin?.isOutputLoosely() && to > from + Configuration.distanceThreshold) {
this.element.target = /** @type {KnotPinTemplate} */(targetPin.template).getoppositePin()
}
}
// Switch visual input/output pins if allowed and makes sense
if (originPin && targetPin) {
let directionsCheckedKnot
if (originPin.isKnot() && originPin.hasUpdated) {
/** @type {KnotNodeTemplate} */(originPin.nodeElement.template).checkSwtichDirectionsVisually()
}
if (targetPin.isKnot() && targetPin.hasUpdated) {
/** @type {KnotNodeTemplate} */(targetPin.nodeElement.template).checkSwtichDirectionsVisually()
}
}
let sameDirection = originPin?.isOutputVisually() == targetPin?.isOutputVisually()
// Actual computation
const dx = Math.max(Math.abs(this.element.originX - this.element.targetX), 1)
const dy = Math.max(Math.abs(this.element.originY - this.element.targetY), 1)
const width = Math.max(dx, Configuration.linkMinWidth)
const fillRatio = dx / width
const xInverted = this.element.originatesFromInput
? this.element.originX < this.element.targetX
: this.element.targetX < this.element.originX
this.element.startPixels = dx < width // If under minimum width
? (width - dx) / 2 // Start from half the empty space
: 0 // Otherwise start from the beginning
const startPercentage = xInverted ? this.element.startPixels + fillRatio * 100 : this.element.startPixels
this.element.startPercentage = startPercentage
const c1 = startPercentage + (sameDirection
? 5
: (
(xInverted
? LinkTemplate.c1DecreasingValue(width)
: 10
)
* fillRatio
)
)
const aspectRatio = dy / Math.max(30, dx)
const c2 = sameDirection
// ? 100 - Math.abs(100 - 2 * startPercentage) + 15
? 100 * LinkTemplate.clampedLine(startPercentage / 50) + 15
: (
LinkTemplate.c2Clamped(dx)
* LinkTemplate.sigmoidPositive(fillRatio * 1.2 + aspectRatio * 0.5, 1.5, 1.8)
+ startPercentage
)
this.element.svgPathD = Configuration.linkRightSVGPath(startPercentage, c1, c2, sameDirection)
}
createInputObjects() {
@@ -117,76 +165,41 @@ export default class LinkTemplate extends IFromToPositionedTemplate {
/** @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 originDX = (changedProperties.get("originX") ?? this.element.originX) - this.element.originX
const originDY = (changedProperties.get("originY") ?? this.element.originY) - this.element.originY
const targetDX = (changedProperties.get("targetX") ?? this.element.targetX) - this.element.targetX
const targetDY = (changedProperties.get("targetY") ?? this.element.targetY) - this.element.targetY
if (originDX != targetDX || originDY != targetDY) {
// Only if it changes shape
this.#calculateSVGPath(changedProperties)
}
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)
* LinkTemplate.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 style = this.element.style
if (changedProperties.has("color")) {
style.setProperty("--ueb-link-color-rgb", this.element.color?.toString() ?? "255, 255, 255")
}
const referencePin = this.element.getOutputPin(true)
if (referencePin) {
this.element.style.setProperty("--ueb-link-color-rgb", LinearColorEntity.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)}`)
style.setProperty("--ueb-start-percentage", `${Math.round(this.element.startPercentage)}%`)
style.setProperty("--ueb-link-start", `${Math.round(this.element.startPixels)}`)
const mirrorV = (this.element.originY > this.element.targetY ? -1 : 1) // If from is below to => mirror
* (this.element.originatesFromInput ? -1 : 1) // Unless fro refers to an input pin
* (this.element.origin?.isInputVisually() && this.element.target?.isInputVisually() ? -1 : 1)
const mirrorH = (this.element.origin?.isInputVisually() && this.element.target?.isInputVisually() ? -1 : 1)
style.setProperty("--ueb-link-scale-y", `${mirrorV}`)
style.setProperty("--ueb-link-scale-x", `${mirrorH}`)
}
render() {
return html`
<svg version="1.2" baseProfile="tiny" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
<path id="${this.#uniqueId}" fill="none" vector-effect="non-scaling-stroke" d="${this.element.svgPathD}" />
<svg version="1.2" baseProfile="tiny" width="100%" height="100%" viewBox="0 0 100 100"
preserveAspectRatio="none"
>
<path id="${this.#uniqueId}" fill="none" vector-effect="non-scaling-stroke"
d="${this.element.svgPathD}"
/>
<use href="#${this.#uniqueId}" class="ueb-link-area" pointer-events="all" />
<use href="#${this.#uniqueId}" class="ueb-link-path" pointer-events="none" />
</svg>

2
js/template/node/CommentNodeTemplate.js Normal file → Executable file
View File

@@ -13,7 +13,7 @@ export default class CommentNodeTemplate extends IResizeableTemplate {
element.classList.add("ueb-node-style-comment", "ueb-node-resizeable")
element.sizeX = 25 * Configuration.gridSize
element.sizeY = 6 * Configuration.gridSize
super.initialize(element) // Keep it at the end because it calls this.getColor() where this.#color must be initialized
super.initialize(element) // Keep it at the end because it needs the color. this.#color must be initialized
}
/** @returns {HTMLElement} */

59
js/template/node/KnotNodeTemplate.js Normal file → Executable file
View File

@@ -1,15 +1,21 @@
import { html } from "lit"
import Configuration from "../../Configuration.js"
import ElementFactory from "../../element/ElementFactory.js"
import KnotPinTemplate from "../pin/KnotPinTemplate.js"
import NodeTemplate from "./NodeTemplate.js"
export default class KnotNodeTemplate extends NodeTemplate {
static #traversedPin = new Set()
/** @type {Boolean?} */
#chainDirection = null // The node is part of a chain connected to an input or output pin
#switchDirectionsVisually = false
get switchDirectionsVisually() {
return this.#switchDirectionsVisually
}
set switchDirectionsVisually(value) {
if (this.#switchDirectionsVisually == value) {
return
}
this.#switchDirectionsVisually = value
this.element.acknowledgeUpdate()
}
/** @type {PinElement} */
#inputPin
@@ -29,24 +35,6 @@ export default class KnotNodeTemplate extends NodeTemplate {
this.element.classList.add("ueb-node-style-minimal")
}
/** @param {PinElement} startingPin */
findDirectionaPin(startingPin) {
if (
startingPin.nodeElement.getType() !== Configuration.paths.knot
|| KnotNodeTemplate.#traversedPin.has(startingPin)
) {
KnotNodeTemplate.#traversedPin.clear()
return true
}
KnotNodeTemplate.#traversedPin.add(startingPin)
for (let pin of startingPin.getLinks().map(l => this.blueprint.getPin(l))) {
if (this.findDirectionaPin(pin)) {
return true
}
}
return false
}
render() {
return html`
<div class="ueb-node-border"></div>
@@ -71,7 +59,28 @@ export default class KnotNodeTemplate extends NodeTemplate {
return result
}
linksChanged() {
checkSwtichDirectionsVisually() {
let leftPinsDelta = 0
let leftPinsCount = 0
let rightPinsDelta = 0
let rightPinsCount = 0
const location = this.outputPin.getLinkLocation()[0]
const links = this.getAllConnectedLinks()
for (const link of links) {
const pin = link.getOtherPin(this.element)
const delta = pin.getLinkLocation()[0] - location
if (pin?.isInput()) {
rightPinsDelta += delta
++rightPinsCount
} else if (pin?.isOutput()) {
leftPinsDelta += delta
++leftPinsCount
}
}
leftPinsDelta /= leftPinsCount
rightPinsDelta /= rightPinsCount
if ((rightPinsDelta < leftPinsDelta) != this.switchDirectionsVisually) {
this.switchDirectionsVisually = rightPinsDelta < leftPinsDelta
}
}
}

0
js/template/node/MetasoundNodeTemplate.js Normal file → Executable file
View File

0
js/template/node/MetasoundOperationTemplate.js Normal file → Executable file
View File

View File

@@ -31,14 +31,14 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
} else {
(pin.isInput() ? this.inputContainer : this.outputContainer).appendChild(this.createPinElement(pin))
}
this.element.acknowledgeReflow()
this.element.acknowledgeUpdate()
}
}
toggleAdvancedDisplayHandler = () => {
this.element.toggleShowAdvancedPinDisplay()
this.element.requestUpdate()
this.element.updateComplete.then(() => this.element.acknowledgeReflow())
this.element.updateComplete.then(() => this.element.acknowledgeUpdate())
}
/** @param {PinEntity<IEntity>} pinEntity */
@@ -57,17 +57,13 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
super.initialize(element)
this.#subtitle = nodeSubtitle(element.entity)
this.element.classList.add(.../** @type {typeof NodeTemplate} */(this.constructor).nodeStyleClasses)
this.element.style.setProperty("--ueb-node-color", this.getColor().cssText)
this.element.style.setProperty("--ueb-node-color", this.element.entity.nodeColor().cssText)
this.pinInserter = this.element.entity.additionalPinInserter()
if (this.pinInserter) {
this.element.classList.add("ueb-node-is-variadic")
}
}
getColor() {
return this.element.entity.nodeColor()
}
render() {
return html`
<div class="ueb-node-border">
@@ -129,7 +125,7 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
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())
this.element.updateComplete.then(() => this.element.acknowledgeUpdate())
}
setupPins() {
@@ -169,5 +165,10 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
.map(pinEntity => this.createPinElement(pinEntity))
}
linksChanged() { }
/** All the link connected to this node */
getAllConnectedLinks() {
const nodeTitle = this.element.nodeTitle
const query = `ueb-link[data-origin-node="${nodeTitle}"],ueb-link[data-target-node="${nodeTitle}"]`
return /** @type {LinkElement[]} */([...this.blueprint.querySelectorAll(query)])
}
}

2
js/template/node/VariableAccessNodeTemplate.js Normal file → Executable file
View File

@@ -21,6 +21,6 @@ export default class VariableAccessNodeTemplate extends VariableManagementNodeTe
setupPins() {
super.setupPins()
let outputPin = this.element.getPinElements().find(p => !p.entity.isHidden() && !p.entity.isExecution())
this.element.style.setProperty("--ueb-node-color", outputPin.getColor().cssText)
this.element.style.setProperty("--ueb-node-color", outputPin.entity.pinColor().cssText)
}
}

0
js/template/node/VariableConversionNodeTemplate.js Normal file → Executable file
View File

0
js/template/node/VariableMangementNodeTemplate.js Normal file → Executable file
View File

0
js/template/node/VariableOperationNodeTemplate.js Normal file → Executable file
View File

6
js/template/pin/BoolPinTemplate.js Normal file → Executable file
View File

@@ -10,7 +10,7 @@ export default class BoolPinTemplate extends PinTemplate {
#input
#onChangeHandler = () => {
const entity = this.element.getDefaultValue()
const entity = this.element.getDefaultValue(true)
entity.value = this.#input.checked
this.element.setDefaultValue(entity)
}
@@ -40,7 +40,9 @@ export default class BoolPinTemplate extends PinTemplate {
renderInput() {
return html`
<input type="checkbox" class="ueb-pin-input-wrapper ueb-pin-input" ?checked="${this.element.defaultValue?.valueOf() === true}" />
<input type="checkbox" class="ueb-pin-input-wrapper ueb-pin-input"
?checked="${this.element.defaultValue?.valueOf() === true}"
/>
`
}
}

0
js/template/pin/DropdownTemplate.js Normal file → Executable file
View File

2
js/template/pin/EnumPinTemplate.js Normal file → Executable file
View File

@@ -35,7 +35,7 @@ export default class EnumPinTemplate extends IInputPinTemplate {
: [k, Utility.formatStringName(k)]
)
?? []
const defaultEntry = this.element.getDefaultValue().toString()
const defaultEntry = this.element.getDefaultValue()?.toString()
if (!this.#dropdownEntries.find(([k, v]) => k === defaultEntry)) {
this.#dropdownEntries.push([defaultEntry, Utility.formatStringName(defaultEntry)])
}

0
js/template/pin/ExecPinTemplate.js Normal file → Executable file
View File

6
js/template/pin/IInputPinTemplate.js Normal file → Executable file
View File

@@ -78,13 +78,13 @@ export default class IInputPinTemplate extends PinTemplate {
}
if (Self.canWrapInput && this.isInputRendered()) {
this.element.addEventListener("input", this.#checkWrapHandler)
this.element.nodeElement.addEventListener(Configuration.nodeReflowEventName, this.#checkWrapHandler)
this.element.nodeElement.addEventListener(Configuration.nodeUpdateEventName, this.#checkWrapHandler)
}
}
cleanup() {
super.cleanup()
this.element.nodeElement.removeEventListener(Configuration.nodeReflowEventName, this.#checkWrapHandler)
this.element.nodeElement.removeEventListener(Configuration.nodeUpdateEventName, this.#checkWrapHandler)
this.element.removeEventListener("input", this.#checkWrapHandler)
this.element.removeEventListener("input", this.#setInput)
this.element.removeEventListener("focusout", this.#setInput)
@@ -112,7 +112,7 @@ export default class IInputPinTemplate extends PinTemplate {
this.setDefaultValue(values.map(v => IInputPinTemplate.stringFromInputToUE(v)), values)
}
this.element.requestUpdate()
this.element.nodeElement.acknowledgeReflow()
this.element.updateComplete.then(() => this.element.nodeElement.acknowledgeUpdate())
}
setDefaultValue(values = [], rawValues = values) {

0
js/template/pin/INumericPinTemplate.js Normal file → Executable file
View File

4
js/template/pin/InputTemplate.js Normal file → Executable file
View File

@@ -32,7 +32,9 @@ export default class InputTemplate extends ITemplate {
super.initialize(element)
this.element.classList.add("ueb-pin-input-content")
this.element.setAttribute("role", "textbox")
this.element.contentEditable = "true"
if (this.element.contentEditable !== "false") {
this.element.contentEditable = "true"
}
}
/** @param {PropertyValues} changedProperties */

0
js/template/pin/Int64PinTemplate.js Normal file → Executable file
View File

0
js/template/pin/IntPinTemplate.js Normal file → Executable file
View File

View File

@@ -1,5 +0,0 @@
import MinimalPinTemplate from "./MinimalPinTemplate.js"
export default class InternalPinTemplate extends MinimalPinTemplate {
}

75
js/template/pin/KnotPinTemplate.js Normal file → Executable file
View File

@@ -1,29 +1,78 @@
import { html } from "lit"
import Utility from "../../Utility.js"
import FunctionReferenceEntity from "../../entity/FunctionReferenceEntity.js"
import ObjectReferenceEntity from "../../entity/ObjectReferenceEntity.js"
import PinTypeEntity from "../../entity/PinTypeEntity.js"
import StringEntity from "../../entity/StringEntity.js"
import MinimalPinTemplate from "./MinimalPinTemplate.js"
/** @extends MinimalPinTemplate<KnotEntity> */
export default class KnotPinTemplate extends MinimalPinTemplate {
static #wildcardPinType = new PinTypeEntity({
PinCategory: new StringEntity("wildcard"),
PinSubCategoryObject: ObjectReferenceEntity.createNoneInstance(),
PinSubCategoryMemberReference: new FunctionReferenceEntity(),
})
/** @param {PinTypeEntity} type */
#setType(type) {
const oppositePin = this.getoppositePin()
this.element.entity.PinType.copyTypeFrom(type)
oppositePin.entity.PinType.copyTypeFrom(type)
this.element.updateType()
oppositePin.updateType()
}
render() {
return this.element.isOutput() ? super.render() : html``
}
getOppositePin() {
/** @param {PropertyValues} changedProperties */
update(changedProperties) {
super.update(changedProperties)
if (changedProperties.has("isLinked")) {
const oppositePin = this.getoppositePin()
if (!this.element.isLinked && !oppositePin.isLinked) {
this.#setType(KnotPinTemplate.#wildcardPinType)
} else if (this.element.isLinked && this.element.pinType == "wildcard") {
const type = this.element
.getLinks()
.map(r => this.blueprint.getPin(r))
.find(p => p && p.pinType != "wildcard")
?.entity
.PinType
if (type) {
/** @type {KnotPinTemplate[]} */
const propagated = [this]
for (let i = 0; i < propagated.length; ++i) {
let current = propagated[i]
current.#setType(type)
current = /** @type {KnotPinTemplate} */(current.getoppositePin().template)
current.#setType(type)
propagated.push(
...current.element.getLinks().map(r => (
/** @type {KnotPinTemplate} */(
this.blueprint.getPin(r).template
)
))
)
}
}
}
}
}
getoppositePin() {
const nodeTemplate = /** @type {KnotNodeTemplate} */(this.element.nodeElement.template)
return this.element.isOutput() ? nodeTemplate.inputPin : nodeTemplate.outputPin
}
getLinkLocation() {
const rect = (
this.element.isInput()
? /** @type {KnotNodeTemplate} */(this.element.nodeElement.template).outputPin.template
: this
)
.iconElement.getBoundingClientRect()
/** @type {Coordinates} */
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])
/** Location on the grid of a link connecting to this pin */
getLinkLocation(oppositeDirection = false) {
if (this.element.isInput()) {
return this.getoppositePin().getLinkLocation(!oppositeDirection)
}
return super.getLinkLocation(oppositeDirection)
}
}

3
js/template/pin/LinearColorPinTemplate.js Normal file → Executable file
View File

@@ -42,7 +42,8 @@ export default class LinearColorPinTemplate extends PinTemplate {
renderInput() {
return html`
<span class="ueb-pin-input-wrapper ueb-pin-input" data-linear-color="${this.element.getDefaultValue()?.toString() ?? nothing}"
<span class="ueb-pin-input-wrapper ueb-pin-input"
data-linear-color="${this.element.getDefaultValue()?.toString() ?? nothing}"
@click="${this.#launchColorPickerWindow}"
style="--ueb-linear-color: rgba(${this.element.getDefaultValue()?.toString() ?? nothing})">
</span>

0
js/template/pin/MinimalPinTemplate.js Normal file → Executable file
View File

0
js/template/pin/NamePinTemplate.js Normal file → Executable file
View File

View File

@@ -121,6 +121,9 @@ export default class PinTemplate extends ITemplate {
if (this.element.nodeElement?.template instanceof VariableOperationNodeTemplate) {
return SVGIcon.operationPin
}
if (this.element.entity.PinType.PinCategory?.toString().toLocaleLowerCase() === "statictype") {
return SVGIcon.staticPin
}
return SVGIcon.genericPin
}
@@ -156,22 +159,27 @@ export default class PinTemplate extends ITemplate {
// When connected, an input may drop its input fields which means the node has to reflow
const node = this.element.nodeElement
this.element.requestUpdate()
this.element.updateComplete.then(() => node.acknowledgeReflow())
this.element.updateComplete.then(() => node.acknowledgeUpdate())
}
if (changedProperties.has("color")) {
this.element.style.setProperty("--ueb-pin-color-rgb", this.element.color.toString())
}
}
/** @param {PropertyValues} changedProperties */
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties)
this.element.style.setProperty("--ueb-pin-color-rgb", this.element.entity.pinColor().cssText)
this.#iconElement = this.element.querySelector(".ueb-pin-icon svg") ?? this.element
this.#wrapperElement = this.element.querySelector(".ueb-pin-wrapper")
}
getLinkLocation() {
const rect = this.iconElement.getBoundingClientRect()
getLinkLocation(oppositeDirection = false) {
const rect = (this.#iconElement ?? this.element).getBoundingClientRect()
/** @type {[Number, Number]} */
const boundingLocation = [this.element.isInput() ? rect.left : rect.right + 1, (rect.top + rect.bottom) / 2]
const boundingLocation = [
this.element.isInputVisually() != oppositeDirection ? 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])
}
@@ -179,4 +187,16 @@ export default class PinTemplate extends ITemplate {
getClickableElement() {
return this.#wrapperElement ?? this.element
}
/** All the link connected to this pin */
getAllConnectedLinks() {
if (!this.element.isLinked) {
return []
}
const nodeTitle = this.element.nodeElement.nodeTitle
const pinId = this.element.pinId
const query = `ueb-link[data-origin-node="${nodeTitle}"][data-origin-pin="${pinId}"],`
+ `ueb-link[data-target-node="${nodeTitle}"][data-target-pin="${pinId}"]`
return /** @type {LinkElement[]} */([...this.blueprint.querySelectorAll(query)])
}
}

View File

@@ -0,0 +1,19 @@
import { html } from "lit"
import PinTemplate from "./PinTemplate.js"
/** @extends PinTemplate<StringEntity> */
export default class ReadonlyNamePinTemplate extends PinTemplate {
setDefaultValue(values = [], rawValues = values) {
}
renderInput() {
return html`
<div class="ueb-pin-input-wrapper ueb-pin-input">
<ueb-input contenteditable="false" .singleLine="${true}" .selectOnFocus="${false}"
.innerText="${this.element.entity.PinName.toString()}">
</ueb-input>
</div>
`
}
}

0
js/template/pin/RealPinTemplate.js Normal file → Executable file
View File

0
js/template/pin/ReferencePinTemplate.js Normal file → Executable file
View File

0
js/template/pin/RotatorPinTemplate.js Normal file → Executable file
View File

11
js/template/pin/StringPinTemplate.js Normal file → Executable file
View File

@@ -2,4 +2,15 @@ import IInputPinTemplate from "./IInputPinTemplate.js"
/** @extends IInputPinTemplate<StringEntity> */
export default class StringPinTemplate extends IInputPinTemplate {
/**
* @param {String[]} values
* @param {String[]} rawValues
*/
setDefaultValue(values = [], rawValues) {
const value = this.element.getDefaultValue()
value.value = values[0]
this.element.setDefaultValue(value)
this.element.requestUpdate()
}
}

0
js/template/pin/Vector2DPinTemplate.js Normal file → Executable file
View File

0
js/template/pin/Vector4DPinTemplate.js Normal file → Executable file
View File

0
js/template/pin/VectorPinTemplate.js Normal file → Executable file
View File