class Connection { constructor(stateA, stateB) { this.stateA = stateA; this.stateB = stateB; this.text = ''; this.lineAngleAdjust = 0; this.parallelPart = 0.5; this.perpendicularPart = 0; } getAnchorPoint() { const dx = this.stateB.x - this.stateA.x; const dy = this.stateB.y - this.stateA.y; const scale = Math.sqrt(dx ** 2 + dy ** 2); return { x: this.stateA.x + dx * this.parallelPart - dy * this.perpendicularPart / scale, y: this.stateA.y + dy * this.parallelPart + dx * this.perpendicularPart / scale, }; } setAnchorPoint(x, y) { const dx = this.stateB.x - this.stateA.x; const dy = this.stateB.y - this.stateA.y; const scale = Math.sqrt(dx ** 2 + dy ** 2); this.parallelPart = (dx * (x - this.stateA.x) + dy * (y - this.stateA.y)) / (scale ** 2); this.perpendicularPart = (dx * (y - this.stateA.y) - dy * (x - this.stateA.x)) / scale; if (this.parallelPart > 0 && this.parallelPart < 1 && Math.abs(this.perpendicularPart) < settings.snapToPadding) { this.lineAngleAdjust = (this.perpendicularPart < 0) * Math.PI; this.perpendicularPart = 0; } } getEndPointsAndCircle() { if (this.perpendicularPart === 0) { const middleX = (this.stateA.x + this.stateB.x) / 2; const middleY = (this.stateA.y + this.stateB.y) / 2; const start = this.stateA.closestPointOnCircle(middleX, middleY); const end = this.stateB.closestPointOnCircle(middleX, middleY); return { isCircle: false, start, end }; } const anchor = this.getAnchorPoint(); const circle = circleFromPoints( {x: this.stateA.x, y: this.stateA.y}, {x: this.stateB.x, y: this.stateB.y}, anchor, ); const isReversed = this.perpendicularPart > 0; const reverseScale = isReversed ? 1 : -1; const startAngle = Math.atan2(this.stateA.y - circle.y, this.stateA.x - circle.x) - reverseScale * radius / circle.radius; const endAngle = Math.atan2(this.stateB.y - circle.y, this.stateB.x - circle.x) + reverseScale * radius / circle.radius; const startX = circle.x + circle.radius * Math.cos(startAngle); const startY = circle.y + circle.radius * Math.sin(startAngle); const endX = circle.x + circle.radius * Math.cos(endAngle); const endY = circle.y + circle.radius * Math.sin(endAngle); return { isCircle: true, start: {x: startX, y: startY}, end: {x: endX, y: endY}, startAngle, endAngle, circle, isReversed, reverseScale, }; } draw() { const stuff = this.getEndPointsAndCircle(); ctx.beginPath(); ctx.strokeStyle = ctx.fillStyle = settings.colors.getColor(this); if (stuff.isCircle) { ctx.arc(stuff.circle.x, stuff.circle.y, stuff.circle.radius, stuff.startAngle, stuff.endAngle, stuff.isReversed); } else { ctx.moveTo(stuff.start.x, stuff.start.y); ctx.lineTo(stuff.end.x, stuff.end.y); } ctx.stroke(); if (stuff.isCircle) { ctx.drawArrow(stuff.end.x, stuff.end.y, stuff.endAngle - stuff.reverseScale * (Math.PI / 2)); } else { ctx.drawArrow(stuff.end.x, stuff.end.y, Math.atan2(stuff.end.y - stuff.start.y, stuff.end.x - stuff.start.x)); } ctx.closePath(); // Draws the text onto the line if(stuff.isCircle) { let startAngle = stuff.startAngle, endAngle = stuff.endAngle; if(endAngle < startAngle) { endAngle += Math.PI * 2; } const textAngle = (startAngle + endAngle) / 2 + stuff.isReversed * Math.PI, textX = stuff.circle.x + stuff.circle.radius * Math.cos(textAngle), textY = stuff.circle.y + stuff.circle.radius * Math.sin(textAngle); ctx.drawText(this.text, textX, textY, textAngle, selectedObject === this); } else { const textX = (stuff.start.x + stuff.end.x) / 2, textY = (stuff.start.y + stuff.end.y) / 2, textAngle = Math.atan2(stuff.end.x - stuff.start.x, stuff.start.y - stuff.end.y); ctx.drawText(this.text, textX, textY, textAngle + this.lineAngleAdjust, selectedObject === this); } } containsPoint(x, y) { const endPoints = this.getEndPointsAndCircle(); if (endPoints.isCircle) { const dx = x - endPoints.circle.x, dy = y - endPoints.circle.y, distance = Math.sqrt(dx ** 2 + dy ** 2) - endPoints.circle.radius; if (Math.abs(distance) < settings.hitTargetPadding) { let angle = Math.atan2(dy, dx), startAngle = endPoints.startAngle, endAngle = endPoints.endAngle; if (endPoints.isReversed) { [startAngle, endAngle] = [endAngle, startAngle]; } if (endAngle < startAngle) { endAngle += Math.PI * 2; } if (angle < startAngle) { angle += Math.PI * 2; } else if (angle > endAngle) { angle -= Math.PI * 2; } return (angle > startAngle && angle < endAngle); } } else { const dx = endPoints.end.x - endPoints.start.x, dy = endPoints.end.y - endPoints.start.y, length = Math.sqrt(dx ** 2 + dy ** 2), percent = (dx * (x - endPoints.start.x) + dy * (y - endPoints.start.y)) / (length ** 2), distance = (dx * (y - endPoints.start.y) - dy * (x - endPoints.start.x)) / length; return (percent > 0 && percent < 1 && Math.abs(distance) < settings.hitTargetPadding); } return false; } }