mirror of
https://github.com/barsdeveloper/ueblueprint.git
synced 2026-02-04 08:50:33 +08:00
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:
@@ -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
0
js/template/IDraggableControlTemplate.js
Normal file → Executable file
0
js/template/IDraggablePositionedTemplate.js
Normal file → Executable file
0
js/template/IDraggablePositionedTemplate.js
Normal file → Executable 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
0
js/template/IResizeableTemplate.js
Normal file → Executable file
0
js/template/ITemplate.js
Normal file → Executable file
0
js/template/ITemplate.js
Normal file → Executable 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
2
js/template/node/CommentNodeTemplate.js
Normal file → Executable 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
59
js/template/node/KnotNodeTemplate.js
Normal file → Executable 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
0
js/template/node/MetasoundNodeTemplate.js
Normal file → Executable file
0
js/template/node/MetasoundOperationTemplate.js
Normal file → Executable file
0
js/template/node/MetasoundOperationTemplate.js
Normal file → Executable 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
2
js/template/node/VariableAccessNodeTemplate.js
Normal file → Executable 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
0
js/template/node/VariableConversionNodeTemplate.js
Normal file → Executable file
0
js/template/node/VariableMangementNodeTemplate.js
Normal file → Executable file
0
js/template/node/VariableMangementNodeTemplate.js
Normal file → Executable file
0
js/template/node/VariableOperationNodeTemplate.js
Normal file → Executable file
0
js/template/node/VariableOperationNodeTemplate.js
Normal file → Executable file
6
js/template/pin/BoolPinTemplate.js
Normal file → Executable file
6
js/template/pin/BoolPinTemplate.js
Normal file → Executable 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
0
js/template/pin/DropdownTemplate.js
Normal file → Executable file
2
js/template/pin/EnumPinTemplate.js
Normal file → Executable file
2
js/template/pin/EnumPinTemplate.js
Normal file → Executable 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
0
js/template/pin/ExecPinTemplate.js
Normal file → Executable file
6
js/template/pin/IInputPinTemplate.js
Normal file → Executable file
6
js/template/pin/IInputPinTemplate.js
Normal file → Executable 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
0
js/template/pin/INumericPinTemplate.js
Normal file → Executable file
4
js/template/pin/InputTemplate.js
Normal file → Executable file
4
js/template/pin/InputTemplate.js
Normal file → Executable 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
0
js/template/pin/Int64PinTemplate.js
Normal file → Executable file
0
js/template/pin/IntPinTemplate.js
Normal file → Executable file
0
js/template/pin/IntPinTemplate.js
Normal file → Executable 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
75
js/template/pin/KnotPinTemplate.js
Normal file → Executable 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
3
js/template/pin/LinearColorPinTemplate.js
Normal file → Executable 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
0
js/template/pin/MinimalPinTemplate.js
Normal file → Executable file
0
js/template/pin/NamePinTemplate.js
Normal file → Executable file
0
js/template/pin/NamePinTemplate.js
Normal file → Executable 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)])
|
||||
}
|
||||
}
|
||||
|
||||
19
js/template/pin/ReadonlyInputPinTemplate.js
Executable file
19
js/template/pin/ReadonlyInputPinTemplate.js
Executable 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
0
js/template/pin/RealPinTemplate.js
Normal file → Executable file
0
js/template/pin/ReferencePinTemplate.js
Normal file → Executable file
0
js/template/pin/ReferencePinTemplate.js
Normal file → Executable file
0
js/template/pin/RotatorPinTemplate.js
Normal file → Executable file
0
js/template/pin/RotatorPinTemplate.js
Normal file → Executable file
11
js/template/pin/StringPinTemplate.js
Normal file → Executable file
11
js/template/pin/StringPinTemplate.js
Normal file → Executable 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
0
js/template/pin/Vector2DPinTemplate.js
Normal file → Executable file
0
js/template/pin/Vector4DPinTemplate.js
Normal file → Executable file
0
js/template/pin/Vector4DPinTemplate.js
Normal file → Executable file
0
js/template/pin/VectorPinTemplate.js
Normal file → Executable file
0
js/template/pin/VectorPinTemplate.js
Normal file → Executable file
Reference in New Issue
Block a user