mirror of
https://github.com/barsdeveloper/ueblueprint.git
synced 2026-02-16 02:10:38 +08:00
Simplify arc calculation
This commit is contained in:
16
dist/css/ueb-style.css
vendored
16
dist/css/ueb-style.css
vendored
@@ -176,9 +176,7 @@ ueb-node.ueb-node-style-minimal[data-selected=true] .ueb-node-border {
|
||||
ueb-link {
|
||||
position: absolute;
|
||||
--ueb-link-color: rgb(var(--ueb-link-color-rgb));
|
||||
--ueb-from-input-coefficient: calc(2 * var(--ueb-from-input) - 1);
|
||||
/* when from-y > to-y */
|
||||
--ueb-y-reflected: clamp(0, var(--ueb-from-y) - var(--ueb-to-y) - 1, 1);
|
||||
display: block;
|
||||
margin-left: calc(var(--ueb-link-start) * -1px);
|
||||
min-width: calc(var(--ueb-link-min-width) * 1px);
|
||||
@@ -190,12 +188,11 @@ ueb-link {
|
||||
}
|
||||
|
||||
ueb-link > svg {
|
||||
--ueb-y-reflected-coefficient: calc(2 * var(--ueb-y-reflected) - 1);
|
||||
position: absolute;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
min-height: 1px !important;
|
||||
transform: scaleY(calc(var(--ueb-y-reflected-coefficient) * var(--ueb-from-input-coefficient)));
|
||||
transform: scaleY(var(--ueb-link-scale-y));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -222,10 +219,12 @@ ueb-link[data-dragging=true] .ueb-link-message {
|
||||
}
|
||||
|
||||
.ueb-link-message {
|
||||
--ueb-link-message-top: calc(50% * (var(--ueb-link-scale-y) + 1) + 22px);
|
||||
--ueb-link-message-left: calc(100% - var(--ueb-start-percentage) + 15px);
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% * (1 - var(--ueb-y-reflected)) + 22px);
|
||||
left: calc((1 - var(--ueb-from-input)) * 100% + (var(--ueb-from-input-coefficient)) * var(--ueb-start-percentage) + 15px);
|
||||
top: var(--ueb-link-message-top);
|
||||
left: var(--ueb-link-message-left);
|
||||
border: 1px solid #000;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to bottom, #2a2a2a 0, #151515 50%, #2a2a2a 100%);
|
||||
@@ -234,6 +233,11 @@ ueb-link[data-dragging=true] .ueb-link-message {
|
||||
z-index: 1000000;
|
||||
}
|
||||
|
||||
ueb-link[data-from-input=true] .ueb-link-message {
|
||||
--ueb-link-message-top: calc(-50% * (var(--ueb-link-scale-y) - 1) + 22px);
|
||||
--ueb-link-message-left: calc(var(--ueb-start-percentage) + 15px);
|
||||
}
|
||||
|
||||
.ueb-link-message-icon {
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
|
||||
2
dist/css/ueb-style.css.map
vendored
2
dist/css/ueb-style.css.map
vendored
File diff suppressed because one or more lines are too long
2
dist/css/ueb-style.min.css
vendored
2
dist/css/ueb-style.min.css
vendored
File diff suppressed because one or more lines are too long
2
dist/css/ueb-style.min.css.map
vendored
2
dist/css/ueb-style.min.css.map
vendored
File diff suppressed because one or more lines are too long
286
dist/ueblueprint.js
vendored
286
dist/ueblueprint.js
vendored
@@ -103,9 +103,11 @@ class Configuration {
|
||||
* @param {Number} c1
|
||||
* @param {Number} c2
|
||||
*/
|
||||
static linkRightSVGPath = (start, c1, c2) => {
|
||||
let end = 100 - start;
|
||||
return `M ${start} 0 C ${c1.toFixed(3)} 0, ${c2.toFixed(3)} 0, 50 50 S ${(end - c1 + start).toFixed(3)} 100, `
|
||||
static linkRightSVGPath = (start, c1, c2, arc = false) => {
|
||||
const end = 100 - start;
|
||||
const mid = arc ? 100 : 50;
|
||||
const fin = arc ? end + c1 - start : end - c1 + start;
|
||||
return `M ${start} 0 C ${c1.toFixed(2)} 0, ${c2.toFixed(2)} 0, ${mid} 50 S ${fin.toFixed(2)} 100, `
|
||||
+ `${end.toFixed(3)} 100`
|
||||
}
|
||||
static maxZoom = 7
|
||||
@@ -7687,6 +7689,104 @@ class LinkTemplate extends IFromToPositionedTemplate {
|
||||
this.element.destination = inputPin;
|
||||
}
|
||||
|
||||
/** @param {PropertyValues} changedProperties */
|
||||
#calculateSVGPath(changedProperties) {
|
||||
const originPin = this.element.source;
|
||||
const targetPin = this.element.destination;
|
||||
const isOriginAKnot = originPin?.isKnot();
|
||||
const isTargetAKnot = targetPin?.isKnot();
|
||||
const from = this.element.fromX;
|
||||
const to = this.element.toX;
|
||||
|
||||
// Switch actual input/output pins if allowed and makes sense
|
||||
if (isOriginAKnot && (!targetPin || isTargetAKnot)) {
|
||||
if (originPin?.isInputLoosely() && to > from + Configuration.distanceThreshold) {
|
||||
this.element.source = /** @type {KnotPinTemplate} */(originPin.template).oppositePin();
|
||||
} else if (originPin?.isOutputLoosely() && to < from - Configuration.distanceThreshold) {
|
||||
this.element.source = /** @type {KnotPinTemplate} */(originPin.template).oppositePin();
|
||||
}
|
||||
}
|
||||
if (isTargetAKnot && (!originPin || isOriginAKnot)) {
|
||||
if (targetPin?.isInputLoosely() && to < from - Configuration.distanceThreshold) {
|
||||
this.element.destination = /** @type {KnotPinTemplate} */(targetPin.template).oppositePin();
|
||||
} else if (targetPin?.isOutputLoosely() && to > from + Configuration.distanceThreshold) {
|
||||
this.element.destination = /** @type {KnotPinTemplate} */(targetPin.template).oppositePin();
|
||||
}
|
||||
}
|
||||
|
||||
// Switch visual input/output pins if allowed and makes sense
|
||||
let directionsCheckedKnot;
|
||||
if (
|
||||
originPin?.isKnot()
|
||||
&& !changedProperties.has("fromX")
|
||||
&& changedProperties.has("toX")
|
||||
) {
|
||||
// The target end has moved and origin end is a knot
|
||||
directionsCheckedKnot = originPin.nodeElement;
|
||||
} else if (
|
||||
targetPin?.isKnot()
|
||||
&& changedProperties.has("toX")
|
||||
&& !changedProperties.has("fromX")
|
||||
) {
|
||||
// The source end has moved and target end is a knot
|
||||
directionsCheckedKnot = targetPin.nodeElement;
|
||||
}
|
||||
if (directionsCheckedKnot) {
|
||||
let leftPinsLocation = 0;
|
||||
let leftPinsCount = 0;
|
||||
let rightPinsLocation = 0;
|
||||
let rightPinsCount = 0;
|
||||
const pins = directionsCheckedKnot.template
|
||||
.getAllConnectedLinks()
|
||||
.map(l => l.getOtherPin(directionsCheckedKnot));
|
||||
for (const pin of pins) {
|
||||
if (pin.isInput()) {
|
||||
rightPinsLocation += pin.getLinkLocation()[0];
|
||||
++rightPinsCount;
|
||||
} else if (pin.isOutput()) {
|
||||
leftPinsLocation += pin.getLinkLocation()[0];
|
||||
++leftPinsCount;
|
||||
}
|
||||
}
|
||||
leftPinsLocation /= leftPinsCount;
|
||||
rightPinsLocation /= rightPinsCount;
|
||||
const knotTemplate = /** @type {KnotNodeTemplate} */(directionsCheckedKnot.template);
|
||||
if ((rightPinsLocation < leftPinsLocation) != knotTemplate.switchDirectionsVisually) {
|
||||
knotTemplate.switchDirectionsVisually = rightPinsLocation < leftPinsLocation;
|
||||
}
|
||||
}
|
||||
let sameDirection = originPin?.isInputVisually() == targetPin?.isInputVisually();
|
||||
|
||||
// Actual computation
|
||||
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 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 = sameDirection
|
||||
? (this.element.startPercentage + 50)
|
||||
: (
|
||||
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, sameDirection);
|
||||
}
|
||||
|
||||
createInputObjects() {
|
||||
/** @type {HTMLElement} */
|
||||
const linkArea = this.element.querySelector(".ueb-link-area");
|
||||
@@ -7720,68 +7820,23 @@ 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?.isKnot();
|
||||
const isDestinationAKnot = destinationPin?.isKnot();
|
||||
if (isSourceAKnot && (!destinationPin || isDestinationAKnot)) {
|
||||
if (sourcePin?.isInputLoossly() && to > from + Configuration.distanceThreshold) {
|
||||
this.element.source = /** @type {KnotPinTemplate} */(sourcePin.template).oppositePin();
|
||||
} else if (sourcePin?.isOutputLoosely() && to < from - Configuration.distanceThreshold) {
|
||||
this.element.source = /** @type {KnotPinTemplate} */(sourcePin.template).oppositePin();
|
||||
}
|
||||
}
|
||||
if (isDestinationAKnot && (!sourcePin || isSourceAKnot)) {
|
||||
if (destinationPin?.isInputLoossly() && to < from - Configuration.distanceThreshold) {
|
||||
this.element.destination = /** @type {KnotPinTemplate} */(destinationPin.template).oppositePin();
|
||||
} else if (destinationPin?.isOutputLoosely() && to > from + Configuration.distanceThreshold) {
|
||||
this.element.destination = /** @type {KnotPinTemplate} */(destinationPin.template).oppositePin();
|
||||
}
|
||||
}
|
||||
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 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)}`);
|
||||
const mirrorV = (this.element.fromY > this.element.toY ? -1 : 1) // If from is below to => mirror
|
||||
* (this.element.originatesFromInput ? -1 : 1); // Unless from refers to an input pin
|
||||
this.element.style.setProperty("--ueb-link-scale-y", `${mirrorV}`);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -7878,9 +7933,31 @@ class LinkElement extends IFromToPositionedElement {
|
||||
converter: BooleanEntity.booleanConverter,
|
||||
reflect: true,
|
||||
},
|
||||
originNode: {
|
||||
type: String,
|
||||
attribute: "data-origin-node",
|
||||
reflect: true,
|
||||
},
|
||||
originPin: {
|
||||
type: String,
|
||||
attribute: "data-origin-pin",
|
||||
reflect: true,
|
||||
},
|
||||
targetNode: {
|
||||
type: String,
|
||||
attribute: "data-target-node",
|
||||
reflect: true,
|
||||
},
|
||||
targetPin: {
|
||||
type: String,
|
||||
attribute: "data-target-pin",
|
||||
reflect: true,
|
||||
},
|
||||
originatesFromInput: {
|
||||
type: Boolean,
|
||||
attribute: false,
|
||||
attribute: "data-from-input",
|
||||
converter: BooleanEntity.booleanConverter,
|
||||
reflect: true,
|
||||
},
|
||||
svgPathD: {
|
||||
type: String,
|
||||
@@ -7919,7 +7996,12 @@ class LinkElement extends IFromToPositionedElement {
|
||||
#nodeDragSourceHandler = e => this.addSourceLocation(...e.detail.value)
|
||||
/** @param {UEBDragEvent} e */
|
||||
#nodeDragDestinatonHandler = e => this.addDestinationLocation(...e.detail.value)
|
||||
#nodeReflowSourceHandler = e => this.setSourceLocation()
|
||||
#nodeReflowSourceHandler = e => {
|
||||
if (this.source.isKnot()) {
|
||||
this.originatesFromInput = this.source.isInputVisually();
|
||||
}
|
||||
this.setSourceLocation();
|
||||
}
|
||||
#nodeReflowDestinatonHandler = e => this.setDestinationLocation()
|
||||
|
||||
/** @type {TemplateResult | nothing} */
|
||||
@@ -7933,6 +8015,10 @@ class LinkElement extends IFromToPositionedElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.dragging = false;
|
||||
this.originNode = "";
|
||||
this.originPin = "";
|
||||
this.targetNode = "";
|
||||
this.targetPin = "";
|
||||
this.originatesFromInput = false;
|
||||
this.startPercentage = 0;
|
||||
this.svgPathD = "";
|
||||
@@ -7994,9 +8080,15 @@ class LinkElement extends IFromToPositionedElement {
|
||||
);
|
||||
this.#unlinkPins();
|
||||
}
|
||||
isDestinationPin
|
||||
? this.#destination = pin
|
||||
: this.#source = pin;
|
||||
if (isDestinationPin) {
|
||||
this.#destination = pin;
|
||||
this.targetNode = pin?.nodeElement.nodeTitle;
|
||||
this.targetPin = pin?.pinId.toString();
|
||||
} else {
|
||||
this.#source = pin;
|
||||
this.originNode = pin?.nodeElement.nodeTitle;
|
||||
this.originPin = pin?.pinId.toString();
|
||||
}
|
||||
if (getCurrentPin()) {
|
||||
const nodeElement = getCurrentPin().getNodeElement();
|
||||
nodeElement.addEventListener(Configuration.removeEventName, this.#nodeDeleteHandler);
|
||||
@@ -8010,7 +8102,7 @@ class LinkElement extends IFromToPositionedElement {
|
||||
);
|
||||
isDestinationPin
|
||||
? this.setDestinationLocation()
|
||||
: (this.setSourceLocation(), this.originatesFromInput = this.source.isInput());
|
||||
: (this.setSourceLocation(), this.originatesFromInput = this.source.isInputVisually());
|
||||
this.#linkPins();
|
||||
}
|
||||
}
|
||||
@@ -8107,6 +8199,16 @@ class LinkElement extends IFromToPositionedElement {
|
||||
this.source = pin;
|
||||
}
|
||||
|
||||
/** @param {NodeElement} pin */
|
||||
getOtherPin(pin) {
|
||||
if (this.source?.nodeElement === pin) {
|
||||
return this.destination
|
||||
}
|
||||
if (this.destination?.nodeElement === pin) {
|
||||
return this.source
|
||||
}
|
||||
}
|
||||
|
||||
startDragging() {
|
||||
this.dragging = true;
|
||||
}
|
||||
@@ -8977,6 +9079,13 @@ class NodeTemplate extends ISelectableDraggableTemplate {
|
||||
}
|
||||
|
||||
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)])
|
||||
}
|
||||
}
|
||||
|
||||
class IResizeableTemplate extends NodeTemplate {
|
||||
@@ -9612,6 +9721,18 @@ 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)])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9721,10 +9842,17 @@ class KnotPinTemplate extends MinimalPinTemplate {
|
||||
|
||||
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.acknowledgeReflow();
|
||||
}
|
||||
|
||||
/** @type {PinElement} */
|
||||
#inputPin
|
||||
@@ -9744,21 +9872,6 @@ class KnotNodeTemplate extends NodeTemplate {
|
||||
this.element.classList.add("ueb-node-style-minimal");
|
||||
}
|
||||
|
||||
/** @param {PinElement} startingPin */
|
||||
findDirectionaPin(startingPin) {
|
||||
if (startingPin.isKnot() || 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 x`
|
||||
<div class="ueb-node-border"></div>
|
||||
@@ -13294,10 +13407,18 @@ class PinElement extends IElement {
|
||||
}
|
||||
|
||||
/** @returns {boolean} True when the pin is the input part of a knot that can switch direction */
|
||||
isInputLoossly() {
|
||||
isInputLoosely() {
|
||||
return this.isInput(false) && this.isInput(true) === undefined
|
||||
}
|
||||
|
||||
/** @returns {boolean} True when the pin is input and if it is a knot it appears input */
|
||||
isInputVisually() {
|
||||
const template = /** @type {KnotNodeTemplate} */(this.nodeElement.template);
|
||||
const isKnot = this.isKnot();
|
||||
return isKnot && this.isInput() != template.switchDirectionsVisually
|
||||
|| !isKnot && this.isInput()
|
||||
}
|
||||
|
||||
isOutput(ignoreKnots = false) {
|
||||
/** @type {PinElement} */
|
||||
let result = this;
|
||||
@@ -13314,6 +13435,15 @@ class PinElement extends IElement {
|
||||
return this.isOutput(false) && this.isOutput(true) === undefined
|
||||
}
|
||||
|
||||
/** @returns {boolean} True when the pin is output and if it is a knot it appears output */
|
||||
isOutputVisually() {
|
||||
const template = /** @type {KnotNodeTemplate} */(this.nodeElement.template);
|
||||
const isKnot = this.isKnot();
|
||||
return isKnot && this.isOutput() != template.switchDirectionsVisually
|
||||
|| !isKnot && this.isOutput()
|
||||
}
|
||||
|
||||
|
||||
/** @returns {value is InstanceType<PinElement<>>} */
|
||||
isKnot() {
|
||||
return this.nodeElement?.getType() == Configuration.paths.knot
|
||||
|
||||
10
dist/ueblueprint.min.js
vendored
10
dist/ueblueprint.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -79,9 +79,11 @@ export default class Configuration {
|
||||
* @param {Number} c1
|
||||
* @param {Number} c2
|
||||
*/
|
||||
static linkRightSVGPath = (start, c1, c2) => {
|
||||
let end = 100 - start
|
||||
return `M ${start} 0 C ${c1.toFixed(3)} 0, ${c2.toFixed(3)} 0, 50 50 S ${(end - c1 + start).toFixed(3)} 100, `
|
||||
static linkRightSVGPath = (start, c1, c2, arc = false) => {
|
||||
const end = 100 - start
|
||||
const mid = arc ? 100 : 50
|
||||
const fin = arc ? end + c1 - start : end - c1 + start
|
||||
return `M ${start} 0 C ${c1.toFixed(2)} 0, ${c2.toFixed(2)} 0, ${mid} 50 S ${fin.toFixed(2)} 100, `
|
||||
+ `${end.toFixed(3)} 100`
|
||||
}
|
||||
static maxZoom = 7
|
||||
|
||||
@@ -17,9 +17,31 @@ export default class LinkElement extends IFromToPositionedElement {
|
||||
converter: BooleanEntity.booleanConverter,
|
||||
reflect: true,
|
||||
},
|
||||
originNode: {
|
||||
type: String,
|
||||
attribute: "data-origin-node",
|
||||
reflect: true,
|
||||
},
|
||||
originPin: {
|
||||
type: String,
|
||||
attribute: "data-origin-pin",
|
||||
reflect: true,
|
||||
},
|
||||
targetNode: {
|
||||
type: String,
|
||||
attribute: "data-target-node",
|
||||
reflect: true,
|
||||
},
|
||||
targetPin: {
|
||||
type: String,
|
||||
attribute: "data-target-pin",
|
||||
reflect: true,
|
||||
},
|
||||
originatesFromInput: {
|
||||
type: Boolean,
|
||||
attribute: false,
|
||||
attribute: "data-from-input",
|
||||
converter: BooleanEntity.booleanConverter,
|
||||
reflect: true,
|
||||
},
|
||||
svgPathD: {
|
||||
type: String,
|
||||
@@ -58,7 +80,12 @@ export default class LinkElement extends IFromToPositionedElement {
|
||||
#nodeDragSourceHandler = e => this.addSourceLocation(...e.detail.value)
|
||||
/** @param {UEBDragEvent} e */
|
||||
#nodeDragDestinatonHandler = e => this.addDestinationLocation(...e.detail.value)
|
||||
#nodeReflowSourceHandler = e => this.setSourceLocation()
|
||||
#nodeReflowSourceHandler = e => {
|
||||
if (this.source.isKnot()) {
|
||||
this.originatesFromInput = this.source.isInputVisually()
|
||||
}
|
||||
this.setSourceLocation()
|
||||
}
|
||||
#nodeReflowDestinatonHandler = e => this.setDestinationLocation()
|
||||
|
||||
/** @type {TemplateResult | nothing} */
|
||||
@@ -72,6 +99,10 @@ export default class LinkElement extends IFromToPositionedElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.dragging = false
|
||||
this.originNode = ""
|
||||
this.originPin = ""
|
||||
this.targetNode = ""
|
||||
this.targetPin = ""
|
||||
this.originatesFromInput = false
|
||||
this.startPercentage = 0
|
||||
this.svgPathD = ""
|
||||
@@ -133,9 +164,15 @@ export default class LinkElement extends IFromToPositionedElement {
|
||||
)
|
||||
this.#unlinkPins()
|
||||
}
|
||||
isDestinationPin
|
||||
? this.#destination = pin
|
||||
: this.#source = pin
|
||||
if (isDestinationPin) {
|
||||
this.#destination = pin
|
||||
this.targetNode = pin?.nodeElement.nodeTitle
|
||||
this.targetPin = pin?.pinId.toString()
|
||||
} else {
|
||||
this.#source = pin
|
||||
this.originNode = pin?.nodeElement.nodeTitle
|
||||
this.originPin = pin?.pinId.toString()
|
||||
}
|
||||
if (getCurrentPin()) {
|
||||
const nodeElement = getCurrentPin().getNodeElement()
|
||||
nodeElement.addEventListener(Configuration.removeEventName, this.#nodeDeleteHandler)
|
||||
@@ -149,7 +186,7 @@ export default class LinkElement extends IFromToPositionedElement {
|
||||
)
|
||||
isDestinationPin
|
||||
? this.setDestinationLocation()
|
||||
: (this.setSourceLocation(), this.originatesFromInput = this.source.isInput())
|
||||
: (this.setSourceLocation(), this.originatesFromInput = this.source.isInputVisually())
|
||||
this.#linkPins()
|
||||
}
|
||||
}
|
||||
@@ -246,6 +283,16 @@ export default class LinkElement extends IFromToPositionedElement {
|
||||
this.source = pin
|
||||
}
|
||||
|
||||
/** @param {NodeElement} pin */
|
||||
getOtherPin(pin) {
|
||||
if (this.source?.nodeElement === pin) {
|
||||
return this.destination
|
||||
}
|
||||
if (this.destination?.nodeElement === pin) {
|
||||
return this.source
|
||||
}
|
||||
}
|
||||
|
||||
startDragging() {
|
||||
this.dragging = true
|
||||
}
|
||||
|
||||
@@ -148,10 +148,18 @@ export default class PinElement extends IElement {
|
||||
}
|
||||
|
||||
/** @returns {boolean} True when the pin is the input part of a knot that can switch direction */
|
||||
isInputLoossly() {
|
||||
isInputLoosely() {
|
||||
return this.isInput(false) && this.isInput(true) === undefined
|
||||
}
|
||||
|
||||
/** @returns {boolean} True when the pin is input and if it is a knot it appears input */
|
||||
isInputVisually() {
|
||||
const template = /** @type {KnotNodeTemplate} */(this.nodeElement.template)
|
||||
const isKnot = this.isKnot()
|
||||
return isKnot && this.isInput() != template.switchDirectionsVisually
|
||||
|| !isKnot && this.isInput()
|
||||
}
|
||||
|
||||
isOutput(ignoreKnots = false) {
|
||||
/** @type {PinElement} */
|
||||
let result = this
|
||||
@@ -168,6 +176,15 @@ export default class PinElement extends IElement {
|
||||
return this.isOutput(false) && this.isOutput(true) === undefined
|
||||
}
|
||||
|
||||
/** @returns {boolean} True when the pin is output and if it is a knot it appears output */
|
||||
isOutputVisually() {
|
||||
const template = /** @type {KnotNodeTemplate} */(this.nodeElement.template)
|
||||
const isKnot = this.isKnot()
|
||||
return isKnot && this.isOutput() != template.switchDirectionsVisually
|
||||
|| !isKnot && this.isOutput()
|
||||
}
|
||||
|
||||
|
||||
/** @returns {value is InstanceType<PinElement<>>} */
|
||||
isKnot() {
|
||||
return this.nodeElement?.getType() == Configuration.paths.knot
|
||||
|
||||
@@ -84,6 +84,104 @@ export default class LinkTemplate extends IFromToPositionedTemplate {
|
||||
this.element.destination = inputPin
|
||||
}
|
||||
|
||||
/** @param {PropertyValues} changedProperties */
|
||||
#calculateSVGPath(changedProperties) {
|
||||
const originPin = this.element.source
|
||||
const targetPin = this.element.destination
|
||||
const isOriginAKnot = originPin?.isKnot()
|
||||
const isTargetAKnot = targetPin?.isKnot()
|
||||
const from = this.element.fromX
|
||||
const to = this.element.toX
|
||||
|
||||
// Switch actual input/output pins if allowed and makes sense
|
||||
if (isOriginAKnot && (!targetPin || isTargetAKnot)) {
|
||||
if (originPin?.isInputLoosely() && to > from + Configuration.distanceThreshold) {
|
||||
this.element.source = /** @type {KnotPinTemplate} */(originPin.template).oppositePin()
|
||||
} else if (originPin?.isOutputLoosely() && to < from - Configuration.distanceThreshold) {
|
||||
this.element.source = /** @type {KnotPinTemplate} */(originPin.template).oppositePin()
|
||||
}
|
||||
}
|
||||
if (isTargetAKnot && (!originPin || isOriginAKnot)) {
|
||||
if (targetPin?.isInputLoosely() && to < from - Configuration.distanceThreshold) {
|
||||
this.element.destination = /** @type {KnotPinTemplate} */(targetPin.template).oppositePin()
|
||||
} else if (targetPin?.isOutputLoosely() && to > from + Configuration.distanceThreshold) {
|
||||
this.element.destination = /** @type {KnotPinTemplate} */(targetPin.template).oppositePin()
|
||||
}
|
||||
}
|
||||
|
||||
// Switch visual input/output pins if allowed and makes sense
|
||||
let directionsCheckedKnot
|
||||
if (
|
||||
originPin?.isKnot()
|
||||
&& !changedProperties.has("fromX")
|
||||
&& changedProperties.has("toX")
|
||||
) {
|
||||
// The target end has moved and origin end is a knot
|
||||
directionsCheckedKnot = originPin.nodeElement
|
||||
} else if (
|
||||
targetPin?.isKnot()
|
||||
&& changedProperties.has("toX")
|
||||
&& !changedProperties.has("fromX")
|
||||
) {
|
||||
// The source end has moved and target end is a knot
|
||||
directionsCheckedKnot = targetPin.nodeElement
|
||||
}
|
||||
if (directionsCheckedKnot) {
|
||||
let leftPinsLocation = 0
|
||||
let leftPinsCount = 0
|
||||
let rightPinsLocation = 0
|
||||
let rightPinsCount = 0
|
||||
const pins = directionsCheckedKnot.template
|
||||
.getAllConnectedLinks()
|
||||
.map(l => l.getOtherPin(directionsCheckedKnot))
|
||||
for (const pin of pins) {
|
||||
if (pin.isInput()) {
|
||||
rightPinsLocation += pin.getLinkLocation()[0]
|
||||
++rightPinsCount
|
||||
} else if (pin.isOutput()) {
|
||||
leftPinsLocation += pin.getLinkLocation()[0]
|
||||
++leftPinsCount
|
||||
}
|
||||
}
|
||||
leftPinsLocation /= leftPinsCount
|
||||
rightPinsLocation /= rightPinsCount
|
||||
const knotTemplate = /** @type {KnotNodeTemplate} */(directionsCheckedKnot.template)
|
||||
if ((rightPinsLocation < leftPinsLocation) != knotTemplate.switchDirectionsVisually) {
|
||||
knotTemplate.switchDirectionsVisually = rightPinsLocation < leftPinsLocation
|
||||
}
|
||||
}
|
||||
let sameDirection = originPin?.isInputVisually() == targetPin?.isInputVisually()
|
||||
|
||||
// Actual computation
|
||||
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 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 = sameDirection
|
||||
? (this.element.startPercentage + 50)
|
||||
: (
|
||||
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, sameDirection)
|
||||
}
|
||||
|
||||
createInputObjects() {
|
||||
/** @type {HTMLElement} */
|
||||
const linkArea = this.element.querySelector(".ueb-link-area")
|
||||
@@ -117,68 +215,23 @@ 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?.isKnot()
|
||||
const isDestinationAKnot = destinationPin?.isKnot()
|
||||
if (isSourceAKnot && (!destinationPin || isDestinationAKnot)) {
|
||||
if (sourcePin?.isInputLoossly() && to > from + Configuration.distanceThreshold) {
|
||||
this.element.source = /** @type {KnotPinTemplate} */(sourcePin.template).oppositePin()
|
||||
} else if (sourcePin?.isOutputLoosely() && to < from - Configuration.distanceThreshold) {
|
||||
this.element.source = /** @type {KnotPinTemplate} */(sourcePin.template).oppositePin()
|
||||
}
|
||||
}
|
||||
if (isDestinationAKnot && (!sourcePin || isSourceAKnot)) {
|
||||
if (destinationPin?.isInputLoossly() && to < from - Configuration.distanceThreshold) {
|
||||
this.element.destination = /** @type {KnotPinTemplate} */(destinationPin.template).oppositePin()
|
||||
} else if (destinationPin?.isOutputLoosely() && to > from + Configuration.distanceThreshold) {
|
||||
this.element.destination = /** @type {KnotPinTemplate} */(destinationPin.template).oppositePin()
|
||||
}
|
||||
}
|
||||
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 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)}`)
|
||||
const mirrorV = (this.element.fromY > this.element.toY ? -1 : 1) // If from is below to => mirror
|
||||
* (this.element.originatesFromInput ? -1 : 1) // Unless from refers to an input pin
|
||||
this.element.style.setProperty("--ueb-link-scale-y", `${mirrorV}`)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -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.acknowledgeReflow()
|
||||
}
|
||||
|
||||
/** @type {PinElement} */
|
||||
#inputPin
|
||||
@@ -29,21 +35,6 @@ export default class KnotNodeTemplate extends NodeTemplate {
|
||||
this.element.classList.add("ueb-node-style-minimal")
|
||||
}
|
||||
|
||||
/** @param {PinElement} startingPin */
|
||||
findDirectionaPin(startingPin) {
|
||||
if (startingPin.isKnot() || 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>
|
||||
|
||||
@@ -170,4 +170,11 @@ export default class NodeTemplate extends ISelectableDraggableTemplate {
|
||||
}
|
||||
|
||||
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)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,4 +182,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)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
ueb-link {
|
||||
position: absolute;
|
||||
--ueb-link-color: rgb(var(--ueb-link-color-rgb));
|
||||
--ueb-from-input-coefficient: calc(2 * var(--ueb-from-input) - 1);
|
||||
/* when from-y > to-y */
|
||||
--ueb-y-reflected: clamp(0, var(--ueb-from-y) - var(--ueb-to-y) - 1, 1);
|
||||
display: block;
|
||||
margin-left: calc(var(--ueb-link-start) * -1px);
|
||||
min-width: calc(var(--ueb-link-min-width) * 1px);
|
||||
@@ -17,12 +15,11 @@ ueb-link {
|
||||
}
|
||||
|
||||
ueb-link>svg {
|
||||
--ueb-y-reflected-coefficient: calc(2 * var(--ueb-y-reflected) - 1);
|
||||
position: absolute;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
min-height: 1px !important;
|
||||
transform: scaleY(calc(var(--ueb-y-reflected-coefficient) * var(--ueb-from-input-coefficient)));
|
||||
transform: scaleY(var(--ueb-link-scale-y));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -49,16 +46,12 @@ ueb-link[data-dragging="true"] .ueb-link-message {
|
||||
}
|
||||
|
||||
.ueb-link-message {
|
||||
--ueb-link-message-top: calc(50% * (var(--ueb-link-scale-y) + 1) + 22px);
|
||||
--ueb-link-message-left: calc(100% - var(--ueb-start-percentage) + 15px);
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% * (1 - var(--ueb-y-reflected)) + 22px);
|
||||
left: calc(
|
||||
/* If originates from an output pin, start with 100% */
|
||||
(1 - var(--ueb-from-input)) * 100%
|
||||
/* If originates from an input pin, then sum, otherwise subtract */
|
||||
+ (var(--ueb-from-input-coefficient)) * var(--ueb-start-percentage)
|
||||
/* Fixed offset */
|
||||
+ 15px);
|
||||
top: var(--ueb-link-message-top);
|
||||
left: var(--ueb-link-message-left);
|
||||
border: 1px solid #000;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to bottom, #2a2a2a 0, #151515 50%, #2a2a2a 100%);
|
||||
@@ -67,6 +60,11 @@ ueb-link[data-dragging="true"] .ueb-link-message {
|
||||
z-index: 1000000;
|
||||
}
|
||||
|
||||
ueb-link[data-from-input="true"] .ueb-link-message {
|
||||
--ueb-link-message-top: calc(-50% * (var(--ueb-link-scale-y) - 1) + 22px);
|
||||
--ueb-link-message-left: calc(var(--ueb-start-percentage) + 15px);
|
||||
}
|
||||
|
||||
.ueb-link-message-icon {
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
|
||||
Reference in New Issue
Block a user