279 lines
7.4 KiB
JavaScript
279 lines
7.4 KiB
JavaScript
/**
|
|
* Finds the intersection point between
|
|
* * a rectangle centered in point B
|
|
* with sides parallel to the x and y axes
|
|
* * a line passing through points A and B (the center of the rectangle)
|
|
*
|
|
* @param width: rectangle width
|
|
* @param height: rectangle height
|
|
* @param xB; rectangle center x coordinate
|
|
* @param yB; rectangle center y coordinate
|
|
* @param xA; point A x coordinate
|
|
* @param yA; point A y coordinate
|
|
* @author Federico Destefanis
|
|
* @see <a href="https://stackoverflow.com/a/31254199/2668213">based on</a>
|
|
*/
|
|
|
|
function lineIntersectionOnRect(width, height, xB, yB, xA, yA) {
|
|
|
|
var w = width / 2;
|
|
var h = height / 2;
|
|
|
|
var dx = xA - xB;
|
|
var dy = yA - yB;
|
|
|
|
//if A=B return B itself
|
|
if (dx == 0 && dy == 0) return {
|
|
x: xB,
|
|
y: yB
|
|
};
|
|
|
|
var tan_phi = h / w;
|
|
var tan_theta = Math.abs(dy / dx);
|
|
|
|
//tell me in which quadrant the A point is
|
|
var qx = Math.sign(dx);
|
|
var qy = Math.sign(dy);
|
|
|
|
|
|
if (tan_theta > tan_phi) {
|
|
xI = xB + (h / tan_theta) * qx;
|
|
yI = yB + h * qy;
|
|
} else {
|
|
xI = xB + w * qx;
|
|
yI = yB + w * tan_theta * qy;
|
|
}
|
|
|
|
return {
|
|
x: xI,
|
|
y: yI
|
|
};
|
|
|
|
}
|
|
|
|
|
|
const angle_between = (x1, y1, x2, y2) => {
|
|
let angle = Math.atan2(y2-y1, x2-x1);
|
|
if (angle < 0) angle += 2*Math.PI
|
|
return angle;
|
|
}
|
|
|
|
const mod = (n, m) => ((n % m) + m) % m;
|
|
|
|
const hits_border_radius = (angle, box_width, box_height, border_radius) => {
|
|
const height = box_height / 2;
|
|
const width = box_width / 2;
|
|
|
|
const p1 = [width, height - border_radius]
|
|
const p2 = [width - border_radius, height]
|
|
|
|
const border_angle1 = angle_between(0,0, p1[0], p1[1])
|
|
const border_angle2 = angle_between(0,0, p2[0], p2[1])
|
|
|
|
return [
|
|
angle,
|
|
mod(angle-(Math.PI/2), Math.PI*2),
|
|
mod(angle-(Math.PI), Math.PI*2),
|
|
mod(angle-(3*Math.PI/2), Math.PI*2)
|
|
].some(angle => border_angle1 < angle && angle < border_angle2);
|
|
}
|
|
|
|
const box_intersection = (angle, box_width, box_height, border_radius) => {
|
|
const half_height = box_height / 2;
|
|
const half_width = box_width / 2;
|
|
|
|
// if (hits_border_radius(angle, box_width, box_height, border_radius)) {
|
|
// return arc_border_box_intersection(angle, half_width, half_height, border_radius)
|
|
// }
|
|
|
|
return straight_border_box_intersection(angle, half_width, half_height)
|
|
}
|
|
|
|
const straight_border_box_intersection = (angle, box_width, box_height) => ({
|
|
y: Math.cos(angle) * box_width,
|
|
x: Math.sin(angle) * box_height,
|
|
});
|
|
|
|
|
|
const arc_border_box_intersection = (angle, box_width, box_height, border_radius) => {
|
|
//TODO:
|
|
return 1;
|
|
}
|
|
|
|
const SVG_WIDTH = 960;
|
|
const SVG_HEIGHT = 500;
|
|
|
|
const BOX_WIDTH = 120;
|
|
const BOX_HEIGHT = 40;
|
|
const BORDER_RADIUS = 10;
|
|
|
|
const MARKER_VIEWBOX = {
|
|
minX: 0,
|
|
minY: -5,
|
|
maxX: 10,
|
|
maxY: 10,
|
|
};
|
|
|
|
const graph = {
|
|
nodes: [
|
|
{
|
|
name: 'from',
|
|
fixed: true,
|
|
x: 100,
|
|
y: 100,
|
|
w: BOX_WIDTH,
|
|
h: BOX_HEIGHT,
|
|
},
|
|
{
|
|
name: 'to',
|
|
fixed: true,
|
|
x: 250,
|
|
y: 250,
|
|
w: BOX_WIDTH,
|
|
h: BOX_HEIGHT,
|
|
}
|
|
],
|
|
links: [
|
|
{
|
|
source: 0,
|
|
target: 1,
|
|
}
|
|
]
|
|
}
|
|
|
|
const generateMarkerId = index => `arrowhead-${index}`.replace(/\s/g, '-');
|
|
|
|
const svg = d3
|
|
.select('body')
|
|
.append('svg')
|
|
.attr('width', SVG_WIDTH)
|
|
.attr('height', SVG_HEIGHT);
|
|
|
|
|
|
/****************************/
|
|
/* Node and link generation */
|
|
/****************************/
|
|
|
|
const link = svg
|
|
.append('svg:g')
|
|
.attr('class', 'links')
|
|
.selectAll('g')
|
|
.data(graph.links, link => `${link.source}-${link.target}`)
|
|
.join('g')
|
|
.call(g => g
|
|
.append('svg:line')
|
|
.attr('class', 'link-line')
|
|
.style('stroke-width', '5')
|
|
.attr('marker-end', (link, index) => `url(#${generateMarkerId(index)})`)
|
|
)
|
|
.call(g => g
|
|
.append('svg:marker')
|
|
.attr('id', (link, index) => generateMarkerId(index))
|
|
.attr('viewBox', `${MARKER_VIEWBOX.minX} ${MARKER_VIEWBOX.minY} ${MARKER_VIEWBOX.maxX} ${MARKER_VIEWBOX.maxY}`)
|
|
.attr('markerWidth', 2)
|
|
.attr('markerHeight', 4)
|
|
.attr('orient', 'auto')
|
|
.append('svg:path')
|
|
.attr("d", "M0,-5L10,0L0,5")
|
|
// .attr('d', 'M 0 5 L 10 0 L 0 -5 Z')
|
|
// .attr('fill', link.color); // Set the fill color to the link's color
|
|
);
|
|
|
|
const forceDragDrop = d3.drag()
|
|
.on('start', (event, node) => {
|
|
if (!event.active) force.alphaTarget(0.03).restart();
|
|
node.fx = event.x;
|
|
node.fy = event.y;
|
|
})
|
|
.on('drag', (event, node) => {
|
|
if (!event.active) force.alphaTarget(0.1).restart();
|
|
node.fx = event.x;
|
|
node.fy = event.y;
|
|
})
|
|
.on('end', (event, node) => {
|
|
if (!event.active) force.alphaTarget(0.03).restart();
|
|
node.fx = null;
|
|
node.fy = null;
|
|
})
|
|
|
|
const nodes = svg
|
|
.append('svg:g')
|
|
.attr('class', 'nodes')
|
|
.selectAll('rect')
|
|
.data(graph.nodes)
|
|
.join('g')
|
|
.call(g => g.append('rect')
|
|
.attr('class', 'node-box')
|
|
.attr('width', node => node.w)
|
|
.attr('height', node => node.h)
|
|
.attr('rx', BORDER_RADIUS)
|
|
.attr('x', node => node.x)
|
|
.attr('y', node => node.y)
|
|
.attr('id', node => node.name)
|
|
.call(forceDragDrop)
|
|
)
|
|
.call(g => g.append('text').text(node => node.name))
|
|
|
|
/********************/
|
|
/* Force Simulation */
|
|
/********************/
|
|
|
|
const forceLink = d3
|
|
.forceLink()
|
|
// .id((node, index) => node.name)
|
|
.strength(_ => 0.002)
|
|
.links(graph.links);
|
|
|
|
const forceNode = d3
|
|
.forceManyBody()
|
|
// .strength(-400);
|
|
|
|
const force = d3
|
|
.forceSimulation(graph.nodes)
|
|
.force('charge', forceNode)
|
|
.force('link', forceLink)
|
|
.on('tick', () => {
|
|
link.selectAll('line')
|
|
.attr('x1', link => link.source.x) // + d.source.w / 2
|
|
.attr('y1', link => link.source.y) // + d.source.h / 2
|
|
.attr('x2', link => link.target.x) // + d.target.w / 2
|
|
.attr('y2', link => link.target.y) // + d.target.h / 2
|
|
.each(link => {
|
|
const markerId = generateMarkerId(link.index);
|
|
|
|
//In radians
|
|
const linkAngle = angle_between(
|
|
link.source.x,
|
|
-link.source.y,
|
|
link.target.x,
|
|
-link.target.y
|
|
);
|
|
|
|
const intersectionPoint = box_intersection(
|
|
linkAngle,
|
|
link.target.w,
|
|
link.target.h,
|
|
0
|
|
);
|
|
|
|
const intersect = lineIntersectionOnRect(BOX_WIDTH, BOX_HEIGHT, link.source.x, link.source.y, link.target.x, link.target.y);
|
|
|
|
const marker = d3.select(`#${markerId}`)
|
|
|
|
let distance;
|
|
|
|
// if(hits_border_radius(linkAngle, link.target.w, link.target.h, BORDER_RADIUS)) {
|
|
// distance = Math.sqrt( ((intersectionPoint.x)**2) + ((intersectionPoint.y) ** 2) );
|
|
// } else {
|
|
distance = Math.sqrt(Math.pow(intersect.x - link.source.x, 2) + Math.pow(intersect.y - link.source.y, 2));
|
|
// }
|
|
marker.attr('refX', _ => {
|
|
const markerHeight = MARKER_VIEWBOX.maxY - MARKER_VIEWBOX.minY
|
|
return distance + markerHeight/2 + 2;
|
|
});
|
|
});
|
|
|
|
nodes.selectAll('.node-box')
|
|
.attr('x', node => node.x - (node.w / 2))
|
|
.attr('y', node => node.y - (node.h / 2))
|
|
}); |