diff --git a/src/config/cytoscape-style.js b/src/config/cytoscape-style.js index 7dacafe..4bbba03 100644 --- a/src/config/cytoscape-style.js +++ b/src/config/cytoscape-style.js @@ -46,6 +46,7 @@ const style = [ style: { curveStyle: 'bezier', targetArrowShape: 'triangle', + arrowScale: 1.2, }, }, { @@ -54,26 +55,113 @@ const style = [ width: 'data(style.thickness)', lineColor: 'data(style.backgroundColor)', targetArrowColor: 'data(style.backgroundColor)', - curveStyle: 'segments', - segmentDistances: 'data(bendData.bendDistance)', + curveStyle: (ele) => { + const source = ele.source(); + const target = ele.target(); + + // Check if there are parallel edges + const parallelEdges = source.edgesWith(target); + const hasParallelEdges = parallelEdges.length > 1; + + // Get positions + const p1 = source.position(); + const p2 = target.position(); + + // Calculate distance between nodes + const distance = Math.sqrt( + (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2, + ); + + // Calculate difference + const dx = Math.abs(p1.x - p2.x); + const dy = Math.abs(p1.y - p2.y); + + // Define a threshold for what counts as "aligned" + const threshold = 10; + + // Check if edge has custom bend data + const bendDistance = ele.data('bendData')?.bendDistance || 0; + const hasBend = Math.abs(bendDistance) > 0; + + // When nodes are very close, always use straight style to prevent edge disappearance + if (distance < 50) { + return 'straight'; + } + + // For parallel edges or edges with bend, use bezier curves + if (hasParallelEdges || hasBend) { + return 'unbundled-bezier'; + } + + // If aligned horizontally OR vertically, be straight + if (dx < threshold || dy < threshold) { + return 'straight'; + } + + // use unbundled-bezier to respect bend points + return 'unbundled-bezier'; + }, + segmentDistances: (ele) => { + // When nodes are very close, don't apply bend to prevent edge disappearance + const source = ele.source(); + const target = ele.target(); + const p1 = source.position(); + const p2 = target.position(); + const distance = Math.sqrt( + (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2, + ); + + if (distance < 50) { + return 0; + } + + return ele.data('bendData.bendDistance'); + }, segmentWeights: 'data(bendData.bendWeight)', edgeDistances: 'node-position', lineStyle: 'data(style.shape)', + controlPointDistances: (ele) => { + // For parallel edges, ensure adequate control point spacing + const bendDistance = ele.data('bendData')?.bendDistance || 0; + return Math.abs(bendDistance) > 0 ? bendDistance : undefined; + }, + controlPointWeights: (ele) => { + const bendWeight = ele.data('bendData')?.bendWeight; + return bendWeight !== undefined ? bendWeight : 0.5; + }, }, }, { selector: 'edge[label]', style: { - label: 'data(label)', + label: (ele) => { + // Get source and target nodes + const source = ele.source(); + const target = ele.target(); + + // Calculate distance between nodes + const p1 = source.position(); + const p2 = target.position(); + const distance = Math.sqrt( + (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2, + ); + + // Define minimum distance threshold (in pixels) + // Below this distance, hide the label to prevent visual clutter + const minDistanceForLabel = 80; + + // Return label only if nodes are far enough apart + return distance >= minDistanceForLabel ? ele.data('label') : ''; + }, edgeTextRotation: 'autorotate', zIndex: 999, + fontSize: 12, textBackgroundOpacity: 1, - color: '#000', + textBackgroundPadding: '3px', + textBorderWidth: 0, + color: '#333', textBackgroundColor: '#fff', textBackgroundShape: 'roundrectangle', - textBorderColor: '#fff', - textBorderWidth: 2, - textBorderOpacity: 1, }, }, { diff --git a/src/config/defaultStyles.js b/src/config/defaultStyles.js index ff7d2dc..c9b7ddc 100644 --- a/src/config/defaultStyles.js +++ b/src/config/defaultStyles.js @@ -9,8 +9,8 @@ const NodeStyle = { }; const EdgeStyle = { - thickness: 1, - backgroundColor: null, + thickness: 2, + backgroundColor: '#555', shape: 'solid', }; diff --git a/src/graph-builder/graph-core/1-core.js b/src/graph-builder/graph-core/1-core.js index 68978da..6d2602e 100644 --- a/src/graph-builder/graph-core/1-core.js +++ b/src/graph-builder/graph-core/1-core.js @@ -39,6 +39,9 @@ class CoreGraph { } // if (cy) this.cy = cy; this.cy = cytoscape({ ...cyOptions, container: element }); + this.cy.on('position', 'node', () => { + this.cy.edges().updateStyle(); + }); this.id = id; this.projectName = projectName; this.authorName = authorName; @@ -81,9 +84,8 @@ class CoreGraph { this.cy.nodeEditing({ resizeToContentCueEnabled: () => false, setWidth: (node, width) => { - // HARD ENFORCEMENT: Snap width every frame during resize - const snappedWidth = this.snapDimensionToGrid(width); - node.data('style', { ...node.data('style'), width: snappedWidth }); + // Allow free resizing during drag - snapping will happen on resizeend + node.data('style', { ...node.data('style'), width }); // Adjust position to maintain edge alignment based on resize handle const resizeType = node.scratch('resizeType'); @@ -91,7 +93,7 @@ class CoreGraph { const currentPos = node.position(); const initialPos = node.scratch('resizeInitialPos'); const initialWidth = node.scratch('width'); - const widthDelta = snappedWidth - initialWidth; + const widthDelta = width - initialWidth; let newX = currentPos.x; if (resizeType.includes('left')) { @@ -99,14 +101,13 @@ class CoreGraph { } else if (resizeType.includes('right')) { newX = initialPos.x + widthDelta / 2; } - node.position({ x: this.snapToGrid(newX), y: currentPos.y }); + node.position({ x: newX, y: currentPos.y }); } - return snappedWidth; + return width; }, setHeight: (node, height) => { - // HARD ENFORCEMENT: Snap height every frame during resize - const snappedHeight = this.snapDimensionToGrid(height); - node.data('style', { ...node.data('style'), height: snappedHeight }); + // Allow free resizing during drag - snapping will happen on resizeend + node.data('style', { ...node.data('style'), height }); // Adjust position to maintain edge alignment based on resize handle const resizeType = node.scratch('resizeType'); @@ -114,7 +115,7 @@ class CoreGraph { const currentPos = node.position(); const initialPos = node.scratch('resizeInitialPos'); const initialHeight = node.scratch('height'); - const heightDelta = snappedHeight - initialHeight; + const heightDelta = height - initialHeight; let newY = currentPos.y; if (resizeType.includes('top')) { @@ -122,9 +123,9 @@ class CoreGraph { } else if (resizeType.includes('bottom')) { newY = initialPos.y + heightDelta / 2; } - node.position({ x: currentPos.x, y: this.snapToGrid(newY) }); + node.position({ x: currentPos.x, y: newY }); } - return snappedHeight; + return height; }, isNoResizeMode(node) { return node.data('type') !== 'ordin'; }, isNoControlsMode(node) { return node.data('type') !== 'ordin'; }, @@ -132,7 +133,7 @@ class CoreGraph { this.cy.gridGuide({ snapToGridOnRelease: true, - snapToGridDuringDrag: true, + snapToGridDuringDrag: false, zoomDash: true, panGrid: true, gridSpacing: this.gridSize, diff --git a/src/graph-builder/graph-core/3-component.js b/src/graph-builder/graph-core/3-component.js index 7b9b976..5a81e92 100644 --- a/src/graph-builder/graph-core/3-component.js +++ b/src/graph-builder/graph-core/3-component.js @@ -16,7 +16,7 @@ class GraphComponent extends GraphCanvas { constructor(...args) { super(...args); - const [,,,,, nodeValidator, edgeValidator] = args; + const [, , , , , nodeValidator, edgeValidator] = args; this.setEdgeNodeValidator({ nodeValidator, edgeValidator }); this.getTid = () => new Date().getTime(); } @@ -70,7 +70,14 @@ class GraphComponent extends GraphCanvas { if (targetID === edge.target().id()) dists.add(edge.data('bendData').bendDistance); else dists.add(-edge.data('bendData').bendDistance); }); - for (let d = 0; ;d += 20) { + + // Calculate optimal spacing based on number of parallel edges + // Base spacing of 60px, increasing with more edges for better visibility + const edgeCount = edges.length; + const baseSpacing = 100; + const spacingIncrement = edgeCount > 4 ? baseSpacing * 1.5 : baseSpacing; + + for (let d = 0; ; d += spacingIncrement) { if (!dists.has(d)) return d; if (!dists.has(-d)) return -d; }