Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions src/config/cytoscape-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const style = [
style: {
curveStyle: 'bezier',
targetArrowShape: 'triangle',
arrowScale: 1.2,
},
},
{
Expand All @@ -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,
},
},
{
Expand Down
4 changes: 2 additions & 2 deletions src/config/defaultStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const NodeStyle = {
};

const EdgeStyle = {
thickness: 1,
backgroundColor: null,
thickness: 2,
backgroundColor: '#555',
shape: 'solid',
};

Expand Down
27 changes: 14 additions & 13 deletions src/graph-builder/graph-core/1-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,58 +84,56 @@ 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');
if (resizeType && (resizeType.includes('left') || resizeType.includes('right'))) {
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')) {
newX = initialPos.x - widthDelta / 2;
} 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');
if (resizeType && (resizeType.includes('top') || resizeType.includes('bottom'))) {
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')) {
newY = initialPos.y - heightDelta / 2;
} 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'; },
});

this.cy.gridGuide({
snapToGridOnRelease: true,
snapToGridDuringDrag: true,
snapToGridDuringDrag: false,
zoomDash: true,
panGrid: true,
gridSpacing: this.gridSize,
Expand Down
11 changes: 9 additions & 2 deletions src/graph-builder/graph-core/3-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
}
Expand Down