merged back lonkwires
This commit is contained in:
@@ -67,8 +67,7 @@ bz-select option:hover{
|
|||||||
color: #FFF;
|
color: #FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
bz-toggler{
|
bz-toggler{
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -138,6 +137,8 @@ bz-toggler div.toggle-switch span.toggle-thumb.turned-on {
|
|||||||
left : 1em;
|
left : 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************************************************************************************/
|
||||||
|
|
||||||
bz-slidepane {
|
bz-slidepane {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -187,6 +188,9 @@ bz-slidepane[side="right"] div.handle {
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************************************************************************************/
|
||||||
|
|
||||||
bz-graflow {
|
bz-graflow {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
246
bzGraflow.js
246
bzGraflow.js
@@ -24,6 +24,13 @@ class BZgraflow extends Buildoz{
|
|||||||
.bzgf-wires-container,
|
.bzgf-wires-container,
|
||||||
.bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; }
|
.bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||||
.bzgf-nodes-container .bzgf-node{ position:absolute; }
|
.bzgf-nodes-container .bzgf-node{ position:absolute; }
|
||||||
|
.bzgf-nodes-container .bzgf-fake-node{
|
||||||
|
position: absolute;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
backgrround: transparent;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
this.shadow.appendChild(style)
|
this.shadow.appendChild(style)
|
||||||
this.nodesContainer = document.createElement('div')
|
this.nodesContainer = document.createElement('div')
|
||||||
@@ -73,7 +80,6 @@ class BZgraflow extends Buildoz{
|
|||||||
for(const tpl of doc.querySelectorAll('template')){
|
for(const tpl of doc.querySelectorAll('template')){
|
||||||
if(tpl.id=='svg-arrows'){
|
if(tpl.id=='svg-arrows'){
|
||||||
this.arrowDefs = tpl.querySelector('defs').cloneNode(true)
|
this.arrowDefs = tpl.querySelector('defs').cloneNode(true)
|
||||||
console.log(this.arrowDefs)
|
|
||||||
this.wiresContainer.appendChild(this.arrowDefs)
|
this.wiresContainer.appendChild(this.arrowDefs)
|
||||||
} else {
|
} else {
|
||||||
const rootEl = tpl.content.querySelector('.bzgf-node')
|
const rootEl = tpl.content.querySelector('.bzgf-node')
|
||||||
@@ -112,10 +118,22 @@ class BZgraflow extends Buildoz{
|
|||||||
return(this.stagedNodes[id])
|
return(this.stagedNodes[id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addFakeNode(nid, x, y, w, h){
|
||||||
|
this.stagedNodes[nid] = document.createElement('div')
|
||||||
|
this.stagedNodes[nid].classList.add('bzgf-fake-node')
|
||||||
|
this.stagedNodes[nid].style.left = `${x}px`
|
||||||
|
this.stagedNodes[nid].style.top = `${y}px`
|
||||||
|
this.stagedNodes[nid].style.width = `${w}px`
|
||||||
|
this.stagedNodes[nid].style.height = `${h}px`
|
||||||
|
this.stagedNodes[nid].dataset.id = nid
|
||||||
|
this.nodesContainer.append(this.stagedNodes[nid])
|
||||||
|
return(this.stagedNodes[nid])
|
||||||
|
}
|
||||||
|
|
||||||
addWire(link){
|
addWire(link){
|
||||||
const [idNode1, idPort1] = link.from
|
const [idNode1, idPort1] = link.from
|
||||||
const [idNode2, idPort2] = link.to
|
const [idNode2, idPort2] = link.to
|
||||||
const path = this.bezier(idNode1, idPort1, idNode2, idPort2, this.getBZAttribute('tension'))
|
const path = this.bezierNodes(idNode1, idPort1, idNode2, idPort2, this.getBZAttribute('tension'))
|
||||||
const id = `${idNode1}_${idNode2}`
|
const id = `${idNode1}_${idNode2}`
|
||||||
this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||||
this.stagedWires[id].setAttribute('d', path)
|
this.stagedWires[id].setAttribute('d', path)
|
||||||
@@ -154,14 +172,14 @@ class BZgraflow extends Buildoz{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bezier(idNode1, idPort1, idNode2, idPort2, tensionMin=60) {
|
bezierNodes(idNode1, idPort1, idNode2, idPort2, tensionMin=60) {
|
||||||
const svgRect = this.wiresContainer.getBoundingClientRect()
|
const svgRect = this.wiresContainer.getBoundingClientRect()
|
||||||
const node1 = this.stagedNodes[idNode1]
|
const node1 = this.stagedNodes[idNode1]
|
||||||
const port1 = node1.ports[idPort1]
|
const port1 = node1.ports[idPort1]
|
||||||
const node2 = this.stagedNodes[idNode2]
|
const node2 = this.stagedNodes[idNode2]
|
||||||
const port2 = node2.ports[idPort2]
|
const port2 = node2.ports[idPort2]
|
||||||
if(!node1 || !node2 || !port1 || !port2) {
|
if(!node1 || !node2 || !port1 || !port2) {
|
||||||
console.warn('Link on bad node / port!', idNode1, idPort1, idNode2, idPort2)
|
console.warn('Link on bad node / port ', idNode1, idPort1, idNode2, idPort2)
|
||||||
return('')
|
return('')
|
||||||
}
|
}
|
||||||
const bb1 = port1.el.getBoundingClientRect()
|
const bb1 = port1.el.getBoundingClientRect()
|
||||||
@@ -170,9 +188,10 @@ class BZgraflow extends Buildoz{
|
|||||||
const y1 = Math.floor(bb1.y + (bb1.height/2)) - svgRect.top
|
const y1 = Math.floor(bb1.y + (bb1.height/2)) - svgRect.top
|
||||||
const x2 = Math.floor(bb2.x + (bb2.width/2)) - svgRect.left
|
const x2 = Math.floor(bb2.x + (bb2.width/2)) - svgRect.left
|
||||||
const y2 = Math.floor(bb2.y + (bb2.height/2)) - svgRect.top
|
const y2 = Math.floor(bb2.y + (bb2.height/2)) - svgRect.top
|
||||||
|
const loop = (idNode1==idNode2) && (idPort1==idPort2)
|
||||||
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
|
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
|
||||||
let tension = dist * 0.4
|
let tension = dist * 0.4
|
||||||
if(tension < tensionMin) tension = tensionMin
|
if(tension < tensionMin) tension = parseInt(tensionMin)
|
||||||
|
|
||||||
const dirVect = {
|
const dirVect = {
|
||||||
n: { x: 0, y: -1 },
|
n: { x: 0, y: -1 },
|
||||||
@@ -184,7 +203,7 @@ class BZgraflow extends Buildoz{
|
|||||||
let c1y = y1 + (dirVect[port1.direction].y * tension)
|
let c1y = y1 + (dirVect[port1.direction].y * tension)
|
||||||
let c2x = x2 + (dirVect[port2.direction].x * tension)
|
let c2x = x2 + (dirVect[port2.direction].x * tension)
|
||||||
let c2y = y2 + (dirVect[port2.direction].y * tension)
|
let c2y = y2 + (dirVect[port2.direction].y * tension)
|
||||||
if((idNode1==idNode2) && (idPort1==idPort2)){ // Special case of self-loop: spread a bit intermediary points
|
if(loop){
|
||||||
if(['n', 's'].includes(port1.direction)) {
|
if(['n', 's'].includes(port1.direction)) {
|
||||||
c1x += tension
|
c1x += tension
|
||||||
c2x -= tension
|
c2x -= tension
|
||||||
@@ -194,10 +213,65 @@ class BZgraflow extends Buildoz{
|
|||||||
c2y -= 1*tension
|
c2y -= 1*tension
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`
|
return(`M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
bezierInterNodes(idNode1, idPort1, idNode2, idPort2, interNodes, orientation='horizontal', tensionMin=60) {
|
||||||
|
const svgRect = this.wiresContainer.getBoundingClientRect()
|
||||||
|
const makeCubicBezier = (x1, y1, x2, y2, orientation) => {
|
||||||
|
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
|
||||||
|
let tension = dist * 0.4
|
||||||
|
if(tension < tensionMin) tension = parseInt(tensionMin)
|
||||||
|
let c1x, c1y, c2x, c2y
|
||||||
|
if(orientation=='horizontal'){
|
||||||
|
c1x = x1 + tension
|
||||||
|
c1y = y1
|
||||||
|
c2x = x2 - tension
|
||||||
|
c2y = y2
|
||||||
|
} else {
|
||||||
|
c1x = x1
|
||||||
|
c1y = y1 + tension
|
||||||
|
c2x = x2
|
||||||
|
c2y = y2 - tension
|
||||||
|
}
|
||||||
|
return(`C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`)
|
||||||
|
}
|
||||||
|
const directPath = this.bezierNodes(idNode1, idPort1, idNode2, idPort2, tensionMin)
|
||||||
|
const startPath = directPath.substring(0,directPath.indexOf('C'))
|
||||||
|
const endPath = directPath.substring(directPath.lastIndexOf(',')+1).trim()
|
||||||
|
let path = startPath
|
||||||
|
let [ , x1, y1] = startPath.split(' ')
|
||||||
|
x1 = parseInt(x1)
|
||||||
|
y1 = parseInt(y1)
|
||||||
|
for(const interNode of interNodes){
|
||||||
|
const bb = this.stagedNodes[interNode].getBoundingClientRect()
|
||||||
|
let x2; let y2;
|
||||||
|
if(orientation=='horizontal'){
|
||||||
|
x2 = bb.x -svgRect.left
|
||||||
|
y2 =Math.floor(bb.y + (bb.height/2)) - svgRect.top
|
||||||
|
path += makeCubicBezier(x1, y1, x2, y2, orientation)
|
||||||
|
x2 += bb.width
|
||||||
|
path += ` L ${x2} ${y2} `
|
||||||
|
} else {
|
||||||
|
x2 = Math.floor(bb.x + (bb.width/2)) - svgRect.left
|
||||||
|
y2 = bb.y - svgRect.top
|
||||||
|
path += makeCubicBezier(x1, y1, x2, y2, orientation)
|
||||||
|
y2 += bb.height
|
||||||
|
path += ` L ${x2} ${y2} `
|
||||||
|
}
|
||||||
|
x1 = x2
|
||||||
|
y1 = y2
|
||||||
|
}
|
||||||
|
let [x2, y2] = endPath.split(' ')
|
||||||
|
x2 = parseInt(x2)
|
||||||
|
y2 = parseInt(y2)
|
||||||
|
path += ' '+makeCubicBezier(x1, y1, x2, y2, orientation)
|
||||||
|
return(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=1000){
|
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=1000){
|
||||||
|
|
||||||
|
// Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them !
|
||||||
let linksWithoutBackEdges
|
let linksWithoutBackEdges
|
||||||
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
|
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
|
||||||
console.warn('Loop(s) detected... Cannot auto-place !')
|
console.warn('Loop(s) detected... Cannot auto-place !')
|
||||||
@@ -206,10 +280,12 @@ class BZgraflow extends Buildoz{
|
|||||||
} else {
|
} else {
|
||||||
linksWithoutBackEdges = this.flow.links
|
linksWithoutBackEdges = this.flow.links
|
||||||
}
|
}
|
||||||
|
|
||||||
const { parents, adj } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges)
|
const { parents, adj } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges)
|
||||||
|
|
||||||
const layers = this.computeLayers(this.flow.nodes, parents)
|
const layers = this.computeLayers(this.flow.nodes, parents)
|
||||||
|
|
||||||
|
// Compute indexes for each layer (int part) & add sub-index for ports
|
||||||
|
// Also compute max width/height for each layer
|
||||||
let maxHeight = 0; let maxWidth = 0
|
let maxHeight = 0; let maxWidth = 0
|
||||||
const layerHeights = []; const layerWidths = [];
|
const layerHeights = []; const layerWidths = [];
|
||||||
const indexes = {} // indexes[nid] = { base: <int>, ports: { [portId]: <float in [0,1)> } }
|
const indexes = {} // indexes[nid] = { base: <int>, ports: { [portId]: <float in [0,1)> } }
|
||||||
@@ -227,37 +303,75 @@ class BZgraflow extends Buildoz{
|
|||||||
layerWidths.push(totWidth)
|
layerWidths.push(totWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If any long-links, create placeholders for skipped layers
|
||||||
|
this.flow.longLinks = this.findLongLinks(this.flow.links)
|
||||||
|
for(const link of this.flow.longLinks){
|
||||||
|
for(const layerIdx of link.skippedLayers){
|
||||||
|
const nid = `longLinkPlaceHolder_${crypto.randomUUID()}`
|
||||||
|
layers[layerIdx].push(nid)
|
||||||
|
link.interNodes.push(nid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder layers to avoid crossings thanks to indexes
|
||||||
this.reorderLayers(layers, parents, indexes, orientation)
|
this.reorderLayers(layers, parents, indexes, orientation)
|
||||||
|
|
||||||
|
// Finally place everything
|
||||||
if(orientation=='horizontal'){
|
if(orientation=='horizontal'){
|
||||||
let x = gapx
|
let x = gapx
|
||||||
for(const [idx, layer] of layers.entries()){
|
for(const [idx, layer] of layers.entries()){
|
||||||
let wMax = 0
|
let wMax = this.getMaxWidth(layer)
|
||||||
let y = ((maxHeight - layerHeights[idx]) / 2) + gapy
|
let y = ((maxHeight - layerHeights[idx]) / 2) + gapy
|
||||||
for(const nid of layer){
|
for(const nid of layer){
|
||||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
if(!nid.startsWith('longLinkPlaceHolder_')) {
|
||||||
wMax = (bb.width > wMax) ? bb.width : wMax
|
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||||
this.moveNode(nid, x, y, tween)
|
this.moveNode(nid, x, y, orientation, tween)
|
||||||
y += gapy + bb.height
|
y += gapy + bb.height
|
||||||
|
} else {
|
||||||
|
this.addFakeNode(nid, x, y, wMax, 10)
|
||||||
|
this.moveNode(nid, x, y, orientation, tween)
|
||||||
|
y += gapy + 10 //TODO
|
||||||
|
}
|
||||||
}
|
}
|
||||||
x += wMax + gapx
|
x += wMax + gapx
|
||||||
}
|
}
|
||||||
} else if(orientation=='vertical'){
|
} else if(orientation=='vertical'){
|
||||||
let y = gapy
|
let y = gapy
|
||||||
for(const [idx, layer] of layers.entries()){
|
for(const [idx, layer] of layers.entries()){
|
||||||
let hMax = 0
|
let hMax = this.getMaxHeight(layer)
|
||||||
let x = ((maxWidth - layerWidths[idx]) / 2) + gapx
|
let x = ((maxWidth - layerWidths[idx]) / 2) + gapx
|
||||||
for(const nid of layer){
|
for(const nid of layer){
|
||||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
if(!nid.startsWith('longLinkPlaceHolder_')){
|
||||||
hMax = (bb.height > hMax) ? bb.height : hMax
|
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||||
this.moveNode(nid, x, y, tween)
|
this.moveNode(nid, x, y, orientation, tween)
|
||||||
x += gapx + bb.width
|
x += gapx + bb.width
|
||||||
|
} else {
|
||||||
|
this.addFakeNode(nid, x, y, 10, hMax)
|
||||||
|
this.moveNode(nid, x, y, orientation, tween)
|
||||||
|
x += gapx + 10 //TODO
|
||||||
|
}
|
||||||
}
|
}
|
||||||
y += hMax + gapy
|
y += hMax + gapy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMaxWidth(layer){
|
||||||
|
return(layer.filter(nid =>
|
||||||
|
!nid.startsWith('longLinkPlaceHolder_'))
|
||||||
|
.map(nid => this.stagedNodes[nid].getBoundingClientRect().width)
|
||||||
|
.reduce((a, b) => a > b ? a : b, 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxHeight(layer){
|
||||||
|
return(layer.filter(nid =>
|
||||||
|
!nid.startsWith('longLinkPlaceHolder_'))
|
||||||
|
.map(nid => this.stagedNodes[nid].getBoundingClientRect().height)
|
||||||
|
.reduce((a, b) => a > b ? a : b, 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
computePortOffsets(nid, orientation = 'horizontal'){
|
computePortOffsets(nid, orientation = 'horizontal'){
|
||||||
const node = this.stagedNodes[nid]
|
const node = this.stagedNodes[nid]
|
||||||
if(!node || !node.ports) return({})
|
if(!node || !node.ports) return({})
|
||||||
@@ -298,10 +412,12 @@ class BZgraflow extends Buildoz{
|
|||||||
const toSwap = []
|
const toSwap = []
|
||||||
for(let i=0; i<layer.length; i++){
|
for(let i=0; i<layer.length; i++){
|
||||||
const nid1 = layer[i]
|
const nid1 = layer[i]
|
||||||
|
if(nid1.startsWith('longLinkPlaceHolder_')) continue
|
||||||
// some parents can be in far layers, but at least one is in the prev layer (by definition of layer)
|
// some parents can be in far layers, but at least one is in the prev layer (by definition of layer)
|
||||||
const pnid1 = parents[nid1].find((nid => layers[lidx-1].includes(nid)))
|
const pnid1 = parents[nid1].find((nid => layers[lidx-1].includes(nid)))
|
||||||
for(let j=i+1; j<layer.length; j++){
|
for(let j=i+1; j<layer.length; j++){
|
||||||
const nid2 = layer[j]
|
const nid2 = layer[j]
|
||||||
|
if(nid2.startsWith('longLinkPlaceHolder_')) continue
|
||||||
const pnid2 = parents[nid2].find((nid => layers[lidx-1].includes(nid)))
|
const pnid2 = parents[nid2].find((nid => layers[lidx-1].includes(nid)))
|
||||||
const link1 = (pnid1) ? this.getLink(pnid1, nid1) : null
|
const link1 = (pnid1) ? this.getLink(pnid1, nid1) : null
|
||||||
const link2 = (pnid2) ? this.getLink(pnid2, nid2) : null
|
const link2 = (pnid2) ? this.getLink(pnid2, nid2) : null
|
||||||
@@ -318,7 +434,7 @@ class BZgraflow extends Buildoz{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveNode(nid, destx, desty, duration = 200, cb) {
|
moveNode(nid, destx, desty, orientation, duration = 200, cb) {
|
||||||
const t0 = performance.now()
|
const t0 = performance.now()
|
||||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||||
const parentbb = this.stagedNodes[nid].parentElement.getBoundingClientRect()
|
const parentbb = this.stagedNodes[nid].parentElement.getBoundingClientRect()
|
||||||
@@ -331,30 +447,38 @@ class BZgraflow extends Buildoz{
|
|||||||
const y = y0 + (desty - y0) * k
|
const y = y0 + (desty - y0) * k
|
||||||
this.stagedNodes[nid].style.left = `${x}px`
|
this.stagedNodes[nid].style.left = `${x}px`
|
||||||
this.stagedNodes[nid].style.top = `${y}px`
|
this.stagedNodes[nid].style.top = `${y}px`
|
||||||
this.updateWires(nid)
|
this.updateWires(nid, orientation)
|
||||||
|
|
||||||
if(p < 1) requestAnimationFrame(frame.bind(this))
|
if(p < 1) requestAnimationFrame(frame.bind(this))
|
||||||
}
|
}
|
||||||
requestAnimationFrame(frame.bind(this))
|
requestAnimationFrame(frame.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
updateWires(nid){
|
updateWires(nid, orientation){
|
||||||
const wires = Object.keys(this.stagedWires)
|
const wires = Object.keys(this.stagedWires)
|
||||||
.filter(id => (id.startsWith(nid+'_')||id.endsWith('_'+nid)))
|
.filter(id => (id.startsWith(nid+'_')||id.endsWith('_'+nid)))
|
||||||
.map(id => this.stagedWires[id])
|
.map(id => this.stagedWires[id])
|
||||||
for(const wire of wires){
|
for(const wire of wires){
|
||||||
const [nid1, nid2] = wire.dataset.id.split('_')
|
const [nid1, nid2] = wire.dataset.id.split('_')
|
||||||
const lnk = this.getLink(nid1, nid2)
|
const lnk = this.getLink(nid1, nid2)
|
||||||
const path = this.bezier(nid1, lnk.from[1], nid2, lnk.to[1], this.getBZAttribute('tension'))
|
const longLink = this.flow.longLinks.find(item => (item.link.from[0] == lnk.from[0] && item.link.from[1] == lnk.from[1] && item.link.to[0] == lnk.to[0] && item.link.to[1] == lnk.to[1]))
|
||||||
wire.setAttribute('d', path)
|
if(longLink) {
|
||||||
|
const path = this.bezierInterNodes(nid1, lnk.from[1], nid2, lnk.to[1], longLink.interNodes, orientation, this.getBZAttribute('tension'))
|
||||||
|
wire.setAttribute('d', path)
|
||||||
|
} else {
|
||||||
|
const path = this.bezierNodes(nid1, lnk.from[1], nid2, lnk.to[1], this.getBZAttribute('tension'))
|
||||||
|
wire.setAttribute('d', path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getLink(nid1, nid2){
|
getLink(nid1, nid2){
|
||||||
return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2))))
|
return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2))))
|
||||||
}
|
}
|
||||||
|
|
||||||
buildGraphStructures(nodes, links) {
|
buildGraphStructures(nodes, links, includeLinkIndexes = false) {
|
||||||
const parents = {}
|
const parents = {}
|
||||||
const adj = {}
|
const adj = {}
|
||||||
nodes.forEach(n => {
|
nodes.forEach(n => {
|
||||||
@@ -362,14 +486,19 @@ class BZgraflow extends Buildoz{
|
|||||||
adj[n.id] = []
|
adj[n.id] = []
|
||||||
})
|
})
|
||||||
|
|
||||||
links.forEach(link => {
|
links.forEach((link, idx) => {
|
||||||
const from = link.from[0]
|
const from = link.from[0]
|
||||||
const to = link.to[0]
|
const to = link.to[0]
|
||||||
parents[to].push(from)
|
if(link.from[0] !== link.to[0]) { // Skip self-loops
|
||||||
adj[from].push(to)
|
parents[to].push(from)
|
||||||
|
if(includeLinkIndexes) {
|
||||||
|
adj[from].push({ to, linkIdx: idx })
|
||||||
|
} else {
|
||||||
|
adj[from].push(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
return({ parents, adj })
|
||||||
return { parents, adj }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
computeLayers(nodes, parents) {
|
computeLayers(nodes, parents) {
|
||||||
@@ -394,14 +523,9 @@ class BZgraflow extends Buildoz{
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasAnyLoop(nodes, links) {
|
hasAnyLoop(nodes, links) {
|
||||||
// self-loops
|
if(links.some(l => l.from[0] === l.to[0])) return(true) // self-loops
|
||||||
if(this.flow.links.some(l => l.from[0] === l.to[0])) return(true)
|
|
||||||
|
|
||||||
// multi-node cycles
|
|
||||||
const adj = {};
|
|
||||||
this.flow.nodes.forEach(node => adj[node.id] = [])
|
|
||||||
this.flow.links.forEach(link => adj[link.from[0]].push(link.to[0]))
|
|
||||||
|
|
||||||
|
const { adj } = this.buildGraphStructures(nodes, links)
|
||||||
const visiting = new Set();
|
const visiting = new Set();
|
||||||
const visited = new Set()
|
const visited = new Set()
|
||||||
const dfs = (nid) => {
|
const dfs = (nid) => {
|
||||||
@@ -420,22 +544,12 @@ class BZgraflow extends Buildoz{
|
|||||||
visited.add(nid)
|
visited.add(nid)
|
||||||
return(false)
|
return(false)
|
||||||
}
|
}
|
||||||
return(this.flow.nodes.map(n => n.id).some(dfs))
|
return(nodes.map(n => n.id).some(dfs))
|
||||||
}
|
}
|
||||||
|
|
||||||
findBackEdges(nodes, links) {
|
findBackEdges(nodes, links) {
|
||||||
// Build adjacency list with link indexes
|
const { adj } = this.buildGraphStructures(nodes, links, true)
|
||||||
const adj = {};
|
const color = {}; nodes.forEach(n => color[n.id] = 'white')
|
||||||
nodes.forEach(node => adj[node.id] = [])
|
|
||||||
for(let [idx, link] of links.entries()){
|
|
||||||
if(link.from[0] !== link.to[0]) { // Skip self-loops
|
|
||||||
adj[link.from[0]].push({ to: link.to[0], linkIdx: idx })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = {}
|
|
||||||
nodes.forEach(n => color[n.id] = 'white')
|
|
||||||
|
|
||||||
const backEdges = []
|
const backEdges = []
|
||||||
|
|
||||||
function dfs(u) {
|
function dfs(u) {
|
||||||
@@ -456,9 +570,41 @@ class BZgraflow extends Buildoz{
|
|||||||
if (color[n.id] === 'white') dfs(n.id)
|
if (color[n.id] === 'white') dfs(n.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
return backEdges
|
return(backEdges)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findLongLinks(links) {
|
||||||
|
let linksWithoutBackEdges
|
||||||
|
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
|
||||||
|
console.warn('Loop(s) detected... Cannot auto-place !')
|
||||||
|
const backEdges = this.findBackEdges(this.flow.nodes, this.flow.links)
|
||||||
|
linksWithoutBackEdges = this.flow.links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0]))
|
||||||
|
} else {
|
||||||
|
linksWithoutBackEdges = this.flow.links
|
||||||
|
}
|
||||||
|
/// Yes that means we ignore long & back links !
|
||||||
|
const { parents } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges)
|
||||||
|
const layers = this.computeLayers(this.flow.nodes, parents)
|
||||||
|
const crossLayerLinks = []
|
||||||
|
for(const link of links){
|
||||||
|
const from = link.from[0]
|
||||||
|
const to = link.to[0]
|
||||||
|
const idx1 = layers.findIndex(layer => layer.includes(from))
|
||||||
|
const idx2 = layers.findIndex(layer => layer.includes(to))
|
||||||
|
if(Math.abs(idx1-idx2)>1) {
|
||||||
|
const lowerIdx = idx1<idx2 ? idx1 : idx2
|
||||||
|
const higherIdx = idx1>idx2 ? idx1 : idx2
|
||||||
|
crossLayerLinks.push({
|
||||||
|
link,
|
||||||
|
linkIdx: link.linkIdx,
|
||||||
|
interNodes: [],
|
||||||
|
skippedLayers: Array.from({ length: higherIdx - lowerIdx - 1 }, (_, i) => lowerIdx + i + 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return(crossLayerLinks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Buildoz.define('graflow', BZgraflow)
|
Buildoz.define('graflow', BZgraflow)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user