Skip to content

Commit e536fdf

Browse files
herzadinataBOCOVO
authored andcommitted
fix(dbml-ext): arrow button target scrolling based on table width fixed
1 parent b6875a1 commit e536fdf

File tree

1 file changed

+80
-37
lines changed

1 file changed

+80
-37
lines changed

packages/json-table-schema-visualizer/src/components/RelationConnection/ConnectionPath.tsx

Lines changed: 80 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Path, Group, Circle, RegularPolygon } from "react-konva";
2-
import { useState, useRef } from "react";
2+
import { useState, useRef, useMemo } from "react";
3+
4+
import type { XYPosition } from "@/types/positions";
35

46
import eventEmitter from "@/events-emitter";
57
import { tableCoordsStore } from "@/stores/tableCoords";
@@ -58,6 +60,62 @@ const ConnectionPath = ({
5860

5961
const colsIndexes = tablesInfo.colsIndexes ?? {};
6062

63+
const countFieldsForTable = (tableName: string): number =>
64+
Object.keys(colsIndexes).filter((k) => k.startsWith(`${tableName}.`))
65+
.length;
66+
67+
const [sourceFieldsCount, targetFieldsCount] = useMemo(
68+
() => [
69+
countFieldsForTable(sourceTableName),
70+
countFieldsForTable(targetTableName),
71+
],
72+
[colsIndexes, sourceTableName, targetTableName],
73+
);
74+
75+
const srcHeight = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * sourceFieldsCount;
76+
const tgtHeight = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * targetFieldsCount;
77+
78+
interface TableBounds {
79+
left: number;
80+
right: number;
81+
top: number;
82+
bottom: number;
83+
}
84+
85+
const buildTableBounds = (
86+
coords: XYPosition,
87+
width: number,
88+
height: number,
89+
): TableBounds => ({
90+
left: coords.x ?? 0,
91+
right: (coords.x ?? 0) + width,
92+
top: coords.y ?? 0,
93+
bottom: (coords.y ?? 0) + height,
94+
});
95+
96+
const distanceToBounds = (point: XYPosition, bounds: TableBounds): number => {
97+
const clampedX = Math.max(bounds.left, Math.min(point.x, bounds.right));
98+
const clampedY = Math.max(bounds.top, Math.min(point.y, bounds.bottom));
99+
return Math.hypot(point.x - clampedX, point.y - clampedY);
100+
};
101+
102+
const resolveTargetByEdgeDistance = (
103+
point: XYPosition,
104+
srcCoords: XYPosition,
105+
tgtCoords: XYPosition,
106+
): string => {
107+
const sourceBounds = buildTableBounds(srcCoords, srcWidth, srcHeight);
108+
const targetBounds = buildTableBounds(tgtCoords, tgtWidth, tgtHeight);
109+
const sourceDistance = distanceToBounds(point, sourceBounds);
110+
const targetDistance = distanceToBounds(point, targetBounds);
111+
return sourceDistance < targetDistance ? targetTableName : sourceTableName;
112+
};
113+
114+
const normalizeAngle = (angle: number): number => {
115+
const normalized = angle % 360;
116+
return normalized < 0 ? normalized + 360 : normalized;
117+
};
118+
61119
const highlight =
62120
hoveredTableName === sourceTableName ||
63121
hoveredTableName === targetTableName ||
@@ -155,8 +213,14 @@ const ConnectionPath = ({
155213
END_LINE_TOLERATE,
156214
);
157215

158-
setBtnPos({ x: localX, y: localY });
159-
if (computedArrow != null) setBtnTarget(computedArrow);
216+
const buttonPoint: XYPosition = { x: localX, y: localY };
217+
setBtnPos(buttonPoint);
218+
const edgeTarget = resolveTargetByEdgeDistance(
219+
buttonPoint,
220+
srcCoords,
221+
tgtCoords,
222+
);
223+
setBtnTarget(edgeTarget);
160224

161225
// Ensure arrow is above relation lines but below table nodes. We look for
162226
// the first child whose name starts with `table-` and set the arrow's
@@ -165,15 +229,25 @@ const ConnectionPath = ({
165229
if (!tooClose && arrowGroupRef.current != null) {
166230
setArrowZIndexRelativeToTables(arrowGroupRef.current);
167231
}
168-
if (computedAngle != null) setBtnAngle(computedAngle + ARROW_ANGLE_OFFSET);
232+
if (computedAngle != null) {
233+
let finalAngle = computedAngle;
234+
if (
235+
computedArrow != null &&
236+
edgeTarget != null &&
237+
edgeTarget !== computedArrow
238+
) {
239+
finalAngle += 180;
240+
}
241+
setBtnAngle(normalizeAngle(finalAngle) + ARROW_ANGLE_OFFSET);
242+
}
169243

170244
setBtnVisible(!tooClose);
171245
};
172246

173247
const handleBtnLeave = () => {
174248
document.body.style.cursor = "default";
175249
setBtnHovering(false);
176-
setIsHovered(false); // unhover both line and circle
250+
setIsHovered(false);
177251
if (hideTimeoutRef.current != null) {
178252
window.clearTimeout(hideTimeoutRef.current);
179253
}
@@ -201,38 +275,7 @@ const ConnectionPath = ({
201275
const btnLocal = btnPos;
202276
let targetToUse: string | null = btnTarget;
203277
if (btnLocal != null) {
204-
// Derive visible fields count from colsIndexes in tablesInfo
205-
const colsIndexes = tablesInfo.colsIndexes ?? {};
206-
const countKeysWithPrefix = (prefix: string) =>
207-
Object.keys(colsIndexes).filter((k) => k.startsWith(`${prefix}.`))
208-
.length;
209-
210-
const srcFields = countKeysWithPrefix(sourceTableName);
211-
const tgtFields = countKeysWithPrefix(targetTableName);
212-
213-
const srcHeight = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * srcFields;
214-
const tgtHeight = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * tgtFields;
215-
216-
const srcCenter = {
217-
x: (srcCoords.x ?? 0) + srcWidth / 2,
218-
y: (srcCoords.y ?? 0) + srcHeight / 2,
219-
};
220-
const tgtCenter = {
221-
x: (tgtCoords.x ?? 0) + tgtWidth / 2,
222-
y: (tgtCoords.y ?? 0) + tgtHeight / 2,
223-
};
224-
225-
const ds = Math.hypot(
226-
(btnLocal.x ?? 0) - srcCenter.x,
227-
(btnLocal.y ?? 0) - srcCenter.y,
228-
);
229-
const dt = Math.hypot(
230-
(btnLocal.x ?? 0) - tgtCenter.x,
231-
(btnLocal.y ?? 0) - tgtCenter.y,
232-
);
233-
234-
// Farther table wins
235-
targetToUse = ds < dt ? targetTableName : sourceTableName;
278+
targetToUse = resolveTargetByEdgeDistance(btnLocal, srcCoords, tgtCoords);
236279
}
237280

238281
if (targetToUse == null || targetToUse.length === 0) return;

0 commit comments

Comments
 (0)