fix(flowchart): prevent arrow overlaps with obstacle-aware orthogonal routing
This commit is contained in:
parent
c103bc02ad
commit
a124cf5e6d
|
|
@ -218,15 +218,32 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
const handles = utils.getResizeHandles(shape);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#2563eb';
|
||||
ctx.lineWidth = 1 / fcState.zoom;
|
||||
const s = 12 / fcState.zoom; // Visual size (larger for touch friendliness)
|
||||
for (const h of handles) {
|
||||
ctx.fillRect(h.x - s/2, h.y - s/2, s, s);
|
||||
ctx.strokeRect(h.x - s/2, h.y - s/2, s, s);
|
||||
if (isSelected || isArrowStart) {
|
||||
// Draw Ports
|
||||
const ports: ('top' | 'bottom' | 'left' | 'right')[] = ['top', 'bottom', 'left', 'right'];
|
||||
ctx.fillStyle = '#3b82f6';
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 1.5 / fcState.zoom;
|
||||
const portSize = 5 / fcState.zoom;
|
||||
|
||||
for (const p of ports) {
|
||||
const pt = utils.getShapePort(shape, p);
|
||||
ctx.beginPath();
|
||||
ctx.arc(pt.x, pt.y, portSize, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
const handles = utils.getResizeHandles(shape);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#2563eb';
|
||||
ctx.lineWidth = 1 / fcState.zoom;
|
||||
const s = 12 / fcState.zoom; // Visual size (larger for touch friendliness)
|
||||
for (const h of handles) {
|
||||
ctx.fillRect(h.x - s/2, h.y - s/2, s, s);
|
||||
ctx.strokeRect(h.x - s/2, h.y - s/2, s, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
|
|
@ -281,6 +298,36 @@
|
|||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Render Label
|
||||
if (arrow.label) {
|
||||
const midIdx = Math.floor(pts.length / 2) - 1;
|
||||
const p1 = pts[midIdx];
|
||||
const p2 = pts[midIdx + 1];
|
||||
const labelX = (p1.x + p2.x) / 2;
|
||||
const labelY = (p1.y + p2.y) / 2;
|
||||
|
||||
ctx.save();
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
const metrics = ctx.measureText(arrow.label);
|
||||
const padding = 4;
|
||||
const bgW = metrics.width + padding * 2;
|
||||
const bgH = 18;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#e2e8f0';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(labelX - bgW / 2, labelY - bgH / 2, bgW, bgH, 4);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#475569';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(arrow.label, labelX, labelY);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Arrowhead logic
|
||||
let last, secondLast;
|
||||
if (arrow.routing === 'curved' && pts.length >= 2) {
|
||||
|
|
|
|||
|
|
@ -43,12 +43,10 @@ export function applyAutoLayout(shapes: Shape[], arrows: Arrow[]): { shapes: Sha
|
|||
}
|
||||
});
|
||||
|
||||
// For arrows, we let the canvas logic or a simple straight line handle it for now
|
||||
// unless we want to use dagre's edge points.
|
||||
// Straight lines are easier to manage with manual dragging later.
|
||||
// For arrows, use orthogonal routing by default to avoid overlapping with nodes
|
||||
arrows.forEach(arrow => {
|
||||
arrow.points = []; // Reset points for straight lines
|
||||
arrow.routing = 'straight';
|
||||
arrow.points = [];
|
||||
arrow.routing = 'orthogonal';
|
||||
});
|
||||
|
||||
return { shapes, arrows };
|
||||
|
|
|
|||
|
|
@ -123,66 +123,102 @@ export function distToSegment(px: number, py: number, x1: number, y1: number, x2
|
|||
return distance(px, py, x1 + t * dx, y1 + t * dy);
|
||||
}
|
||||
|
||||
export function getShapePort(shape: Shape, port: 'top' | 'bottom' | 'left' | 'right') {
|
||||
const b = getShapeBounds(shape);
|
||||
const hw = b.w / 2, hh = b.h / 2;
|
||||
const cx = b.x + hw, cy = b.y + hh;
|
||||
|
||||
if (shape.type === 'circle') {
|
||||
if (port === 'top') return { x: cx, y: cy - shape.r };
|
||||
if (port === 'bottom') return { x: cx, y: cy + shape.r };
|
||||
if (port === 'left') return { x: cx - shape.r, y: cy };
|
||||
if (port === 'right') return { x: cx + shape.r, y: cy };
|
||||
}
|
||||
|
||||
if (port === 'top') return { x: cx, y: b.y };
|
||||
if (port === 'bottom') return { x: cx, y: b.y + b.h };
|
||||
if (port === 'left') return { x: b.x, y: cy };
|
||||
if (port === 'right') return { x: b.x + b.w, y: cy };
|
||||
|
||||
return { x: cx, y: cy };
|
||||
}
|
||||
|
||||
export function getArrowPoints(arrow: Arrow, shapes: Shape[]) {
|
||||
const fromShape = shapes.find(s => s.id === arrow.fromId);
|
||||
const toShape = shapes.find(s => s.id === arrow.toId);
|
||||
|
||||
if (!fromShape || !toShape) return [];
|
||||
|
||||
const fromC = getShapeCenter(fromShape);
|
||||
const toC = getShapeCenter(toShape);
|
||||
|
||||
const hasPoints = arrow.points && arrow.points.length > 0;
|
||||
|
||||
const firstTarget = hasPoints ? arrow.points[0] : toC;
|
||||
const lastTarget = hasPoints ? arrow.points[arrow.points.length - 1] : fromC;
|
||||
|
||||
const fromBorder = getShapeBorderPoint(fromShape, firstTarget.x, firstTarget.y);
|
||||
const toBorder = getShapeBorderPoint(toShape, lastTarget.x, lastTarget.y);
|
||||
|
||||
const pts = [{ x: fromBorder.x, y: fromBorder.y }];
|
||||
|
||||
if (arrow.routing === 'orthogonal') {
|
||||
if (hasPoints) {
|
||||
// If manual points exist, make each segment orthogonal
|
||||
let curr = fromBorder;
|
||||
for (const p of arrow.points) {
|
||||
// Add a midpoint to make it orthogonal
|
||||
if (Math.abs(p.x - curr.x) > Math.abs(p.y - curr.y)) {
|
||||
pts.push({ x: p.x, y: curr.y });
|
||||
} else {
|
||||
pts.push({ x: curr.x, y: p.y });
|
||||
}
|
||||
pts.push(p);
|
||||
curr = p;
|
||||
}
|
||||
// Final segment to toBorder
|
||||
if (Math.abs(toBorder.x - curr.x) > Math.abs(toBorder.y - curr.y)) {
|
||||
pts.push({ x: toBorder.x, y: curr.y });
|
||||
} else {
|
||||
pts.push({ x: curr.x, y: toBorder.y });
|
||||
}
|
||||
} else {
|
||||
// Auto orthogonal (3-segments usually looks best)
|
||||
const midX = (fromBorder.x + toBorder.x) / 2;
|
||||
const midY = (fromBorder.y + toBorder.y) / 2;
|
||||
|
||||
// Decide based on whether shapes are more horizontal or vertical to each other
|
||||
if (Math.abs(fromBorder.x - toBorder.x) > Math.abs(fromBorder.y - toBorder.y)) {
|
||||
pts.push({ x: midX, y: fromBorder.y });
|
||||
pts.push({ x: midX, y: toBorder.y });
|
||||
} else {
|
||||
pts.push({ x: fromBorder.x, y: midY });
|
||||
pts.push({ x: toBorder.x, y: midY });
|
||||
}
|
||||
// Smart Ports selection
|
||||
let fromPort: 'top' | 'bottom' | 'left' | 'right' = 'bottom';
|
||||
let toPort: 'top' | 'bottom' | 'left' | 'right' = 'top';
|
||||
|
||||
if (hasPoints) {
|
||||
const pStart = arrow.points[0];
|
||||
const outputPorts: ('bottom' | 'left' | 'right')[] = ['bottom', 'left', 'right'];
|
||||
let minDist = Infinity;
|
||||
for(const prt of outputPorts) {
|
||||
const pt = getShapePort(fromShape, prt);
|
||||
const d = distance(pStart.x, pStart.y, pt.x, pt.y);
|
||||
if (d < minDist) { minDist = d; fromPort = prt; }
|
||||
}
|
||||
} else {
|
||||
if (hasPoints) {
|
||||
pts.push(...arrow.points);
|
||||
const pEnd = arrow.points[arrow.points.length - 1];
|
||||
const inputPorts: ('top' | 'left' | 'right')[] = ['top', 'left', 'right'];
|
||||
minDist = Infinity;
|
||||
for(const prt of inputPorts) {
|
||||
const pt = getShapePort(toShape, prt);
|
||||
const d = distance(pEnd.x, pEnd.y, pt.x, pt.y);
|
||||
if (d < minDist) { minDist = d; toPort = prt; }
|
||||
}
|
||||
} else if (fromShape.type === 'diamond') {
|
||||
const label = (arrow.label || '').toLowerCase();
|
||||
if (label.includes('tidak') || label.includes('no') || label.includes('false')) {
|
||||
fromPort = (toShape.x < fromShape.x) ? 'left' : 'right';
|
||||
} else if (Math.abs(toShape.x - fromShape.x) > Math.abs(toShape.y - fromShape.y)) {
|
||||
fromPort = (toShape.x < fromShape.x) ? 'left' : 'right';
|
||||
}
|
||||
}
|
||||
|
||||
const p1 = getShapePort(fromShape, fromPort);
|
||||
const p2 = getShapePort(toShape, toPort);
|
||||
const pts = [p1];
|
||||
|
||||
if (hasPoints) {
|
||||
pts.push(...arrow.points);
|
||||
} else if (arrow.routing === 'orthogonal') {
|
||||
const midY = (p1.y + p2.y) / 2;
|
||||
|
||||
if (fromPort === 'bottom' && p1.y < p2.y - 40) {
|
||||
pts.push({ x: p1.x, y: midY });
|
||||
pts.push({ x: p2.x, y: midY });
|
||||
} else if (fromPort === 'bottom' && p1.y >= p2.y - 40) {
|
||||
let minX = p1.x - 50;
|
||||
let maxX = p1.x + 50;
|
||||
shapes.forEach(s => {
|
||||
const b = getShapeBounds(s);
|
||||
if (b.x < minX) minX = b.x;
|
||||
if (b.x + b.w > maxX) maxX = b.x + b.w;
|
||||
});
|
||||
const bypassX = minX - 60; // Route outside the leftmost node
|
||||
pts.push({ x: p1.x, y: p1.y + 20 });
|
||||
pts.push({ x: bypassX, y: p1.y + 20 });
|
||||
pts.push({ x: bypassX, y: p2.y - 20 });
|
||||
pts.push({ x: p2.x, y: p2.y - 20 });
|
||||
} else if (fromPort === 'right' || fromPort === 'left') {
|
||||
const exitDist = 40;
|
||||
const exitX = fromPort === 'right' ? p1.x + exitDist : p1.x - exitDist;
|
||||
pts.push({ x: exitX, y: p1.y });
|
||||
pts.push({ x: exitX, y: p2.y - 20 });
|
||||
pts.push({ x: p2.x, y: p2.y - 20 });
|
||||
} else {
|
||||
pts.push({ x: p1.x, y: midY });
|
||||
pts.push({ x: p2.x, y: midY });
|
||||
}
|
||||
}
|
||||
|
||||
pts.push({ x: toBorder.x, y: toBorder.y });
|
||||
pts.push(p2);
|
||||
return pts;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue