seat-editor 3.4.8 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/constant.d.ts +1 -1
- package/dist/app/graph-view/page.d.ts +1 -0
- package/dist/app/graph-view/page.js +343 -0
- package/dist/app/graph-view/page.jsx +445 -0
- package/dist/app/graph-view-new/constant.d.ts +581 -0
- package/dist/app/graph-view-new/constant.js +6973 -0
- package/dist/app/graph-view-new/page.d.ts +1 -0
- package/dist/app/graph-view-new/page.js +71 -0
- package/dist/app/graph-view-new/page.jsx +98 -0
- package/dist/app/layout.d.ts +1 -1
- package/dist/app/new-board/page.d.ts +1 -1
- package/dist/app/new-board/page.js +43 -7
- package/dist/app/new-board/page.jsx +45 -12
- package/dist/app/old-board/page.d.ts +1 -2
- package/dist/app/only-view/chair.d.ts +1 -1
- package/dist/app/only-view/chair.js +2 -10
- package/dist/app/only-view/page.d.ts +1 -1
- package/dist/app/only-view/user.d.ts +1 -1
- package/dist/app/only-view/user.js +2 -10
- package/dist/app/page.d.ts +1 -1
- package/dist/app/test/page.d.ts +1 -2
- package/dist/app/v2/page.d.ts +1 -1
- package/dist/components/button-tools/index.d.ts +1 -1
- package/dist/components/button-tools/index.js +7 -5
- package/dist/components/button-tools/index.jsx +21 -9
- package/dist/components/form-tools/label.d.ts +1 -1
- package/dist/components/form-tools/label.js +9 -20
- package/dist/components/form-tools/label.jsx +38 -28
- package/dist/components/form-tools/shape.d.ts +1 -1
- package/dist/components/form-tools/shape.js +5 -5
- package/dist/components/form-tools/shape.jsx +8 -8
- package/dist/components/input/number-indicator.d.ts +1 -1
- package/dist/components/joystick/index.d.ts +1 -2
- package/dist/components/layer/index.d.ts +1 -1
- package/dist/components/layer-v2/index.d.ts +1 -1
- package/dist/components/layer-v3/index.d.ts +1 -1
- package/dist/components/layer-v3/index.js +44 -3
- package/dist/components/layer-v3/index.jsx +120 -3
- package/dist/components/layer-v4/index.d.ts +1 -1
- package/dist/components/layer-v5/constant.d.ts +60 -0
- package/dist/components/layer-v5/constant.js +93 -0
- package/dist/components/layer-v5/index.d.ts +24 -0
- package/dist/components/layer-v5/index.js +927 -0
- package/dist/components/layer-v5/index.jsx +1049 -0
- package/dist/components/lib/index.d.ts +1 -1
- package/dist/components/modal-preview/index.d.ts +1 -1
- package/dist/features/board/index.d.ts +1 -1
- package/dist/features/board-v2/index.d.ts +1 -2
- package/dist/features/board-v3/index.d.ts +1 -1
- package/dist/features/board-v3/index.js +350 -72
- package/dist/features/board-v3/index.jsx +369 -75
- package/dist/features/board-v3/resize-element.js +5 -0
- package/dist/features/board-v3/utils.d.ts +8 -0
- package/dist/features/board-v3/utils.js +23 -7
- package/dist/features/navbar/index.d.ts +1 -1
- package/dist/features/package/index.d.ts +3 -1
- package/dist/features/package/index.js +1 -1
- package/dist/features/package/index.jsx +6 -1
- package/dist/features/panel/index.d.ts +9 -1
- package/dist/features/panel/index.js +160 -38
- package/dist/features/panel/index.jsx +173 -46
- package/dist/features/panel/polygon.d.ts +2 -0
- package/dist/features/panel/polygon.js +44 -0
- package/dist/features/panel/polygon.jsx +70 -0
- package/dist/features/panel/select-tool.d.ts +1 -1
- package/dist/features/panel/select-tool.js +3 -0
- package/dist/features/panel/select-tool.jsx +3 -0
- package/dist/features/panel/selected-group.d.ts +1 -1
- package/dist/features/panel/selected-group.js +24 -26
- package/dist/features/panel/selected-group.jsx +56 -51
- package/dist/features/panel/square-circle-tool.d.ts +1 -1
- package/dist/features/panel/table-seat-circle.d.ts +1 -1
- package/dist/features/panel/table-seat-square.d.ts +1 -1
- package/dist/features/panel/text-tool.d.ts +1 -1
- package/dist/features/panel/text-tool.js +17 -2
- package/dist/features/panel/text-tool.jsx +19 -2
- package/dist/features/panel/upload-tool.d.ts +1 -1
- package/dist/features/panel/upload-tool.js +17 -3
- package/dist/features/panel/upload-tool.jsx +23 -4
- package/dist/features/side-tool/index.d.ts +1 -1
- package/dist/features/side-tool/index.js +43 -6
- package/dist/features/side-tool/index.jsx +47 -10
- package/dist/features/view-only/index.d.ts +1 -1
- package/dist/features/view-only-2/index.d.ts +1 -1
- package/dist/features/view-only-3/index.d.ts +1 -1
- package/dist/features/view-only-4/connect-handle.d.ts +13 -0
- package/dist/features/view-only-4/connect-handle.js +23 -0
- package/dist/features/view-only-4/connect-handle.jsx +30 -0
- package/dist/features/view-only-4/connection-layer.d.ts +21 -0
- package/dist/features/view-only-4/connection-layer.js +219 -0
- package/dist/features/view-only-4/connection-layer.jsx +291 -0
- package/dist/features/view-only-4/index.d.ts +99 -0
- package/dist/features/view-only-4/index.js +684 -0
- package/dist/features/view-only-4/index.jsx +722 -0
- package/dist/features/view-only-4/integration-guide.d.ts +0 -0
- package/dist/features/view-only-4/integration-guide.js +0 -0
- package/dist/features/view-only-4/use-connection-graph.d.ts +41 -0
- package/dist/features/view-only-4/use-connection-graph.js +182 -0
- package/dist/features/view-only-4/utils.d.ts +74 -0
- package/dist/features/view-only-4/utils.js +106 -0
- package/dist/features/view-only-5/connect-handle.d.ts +30 -0
- package/dist/features/view-only-5/connect-handle.js +88 -0
- package/dist/features/view-only-5/connect-handle.jsx +96 -0
- package/dist/features/view-only-5/connection-layer.d.ts +34 -0
- package/dist/features/view-only-5/connection-layer.js +182 -0
- package/dist/features/view-only-5/connection-layer.jsx +265 -0
- package/dist/features/view-only-5/index.d.ts +102 -0
- package/dist/features/view-only-5/index.js +585 -0
- package/dist/features/view-only-5/index.jsx +614 -0
- package/dist/features/view-only-5/use-connection-graph.d.ts +57 -0
- package/dist/features/view-only-5/use-connection-graph.js +196 -0
- package/dist/features/view-only-5/utils.d.ts +52 -0
- package/dist/features/view-only-5/utils.js +80 -0
- package/dist/features/view-only-6/connect-handle.d.ts +13 -0
- package/dist/features/view-only-6/connect-handle.js +20 -0
- package/dist/features/view-only-6/connect-handle.jsx +21 -0
- package/dist/features/view-only-6/connection-layer.d.ts +22 -0
- package/dist/features/view-only-6/connection-layer.js +191 -0
- package/dist/features/view-only-6/connection-layer.jsx +244 -0
- package/dist/features/view-only-6/index.d.ts +99 -0
- package/dist/features/view-only-6/index.js +687 -0
- package/dist/features/view-only-6/index.jsx +724 -0
- package/dist/features/view-only-6/use-connection-graph.d.ts +26 -0
- package/dist/features/view-only-6/use-connection-graph.js +103 -0
- package/dist/features/view-only-6/utils.d.ts +66 -0
- package/dist/features/view-only-6/utils.js +96 -0
- package/dist/features/view-only-7/connect-handle.d.ts +13 -0
- package/dist/features/view-only-7/connect-handle.js +23 -0
- package/dist/features/view-only-7/connect-handle.jsx +30 -0
- package/dist/features/view-only-7/connection-layer.d.ts +22 -0
- package/dist/features/view-only-7/connection-layer.js +165 -0
- package/dist/features/view-only-7/connection-layer.jsx +217 -0
- package/dist/features/view-only-7/index.d.ts +99 -0
- package/dist/features/view-only-7/index.js +687 -0
- package/dist/features/view-only-7/index.jsx +724 -0
- package/dist/features/view-only-7/use-connection-graph.d.ts +26 -0
- package/dist/features/view-only-7/use-connection-graph.js +104 -0
- package/dist/features/view-only-7/utils.d.ts +69 -0
- package/dist/features/view-only-7/utils.js +144 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/provider/redux-provider.d.ts +1 -1
- package/dist/provider/store-provider.d.ts +1 -1
- package/dist/seat-editor.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NodeType } from "./utils";
|
|
2
|
+
import { EdgeType } from "./connection-layer";
|
|
3
|
+
export type UseConnectionGraphOptions = {
|
|
4
|
+
svgRef: React.RefObject<SVGSVGElement>;
|
|
5
|
+
getNodeById: (id: string) => NodeType | null;
|
|
6
|
+
onEdgesChange?: (edges: EdgeType[]) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare const useConnectionGraph: ({ svgRef, getNodeById, onEdgesChange, }: UseConnectionGraphOptions) => {
|
|
9
|
+
edges: EdgeType[];
|
|
10
|
+
setEdges: import("react").Dispatch<import("react").SetStateAction<EdgeType[]>>;
|
|
11
|
+
selectedEdge: string;
|
|
12
|
+
connecting: {
|
|
13
|
+
fromId: string;
|
|
14
|
+
};
|
|
15
|
+
mousePos: {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
};
|
|
19
|
+
startConnect: (fromId: string) => void;
|
|
20
|
+
endConnect: (toId: string) => void;
|
|
21
|
+
cancelConnect: () => void;
|
|
22
|
+
selectEdge: (edgeId: string) => void;
|
|
23
|
+
deleteEdge: (edgeId: string) => void;
|
|
24
|
+
clearSelection: () => void;
|
|
25
|
+
onMouseMove: (e: React.MouseEvent<SVGSVGElement> | MouseEvent) => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// hooks/use-connection-graph.ts
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
export const useConnectionGraph = ({ svgRef, getNodeById, onEdgesChange, }) => {
|
|
4
|
+
const [edges, setEdges] = useState([]);
|
|
5
|
+
const [selectedEdge, setSelectedEdge] = useState(null);
|
|
6
|
+
const [connecting, setConnecting] = useState(null);
|
|
7
|
+
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
8
|
+
// Notify parent setiap edges berubah
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
onEdgesChange === null || onEdgesChange === void 0 ? void 0 : onEdgesChange(edges);
|
|
11
|
+
}, [edges]);
|
|
12
|
+
// Keyboard: Delete/Backspace → hapus edge, Escape → cancel connect
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const handleKeyDown = (e) => {
|
|
15
|
+
const tag = e.target.tagName.toLowerCase();
|
|
16
|
+
if (tag === "input" || tag === "textarea")
|
|
17
|
+
return;
|
|
18
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selectedEdge) {
|
|
19
|
+
deleteEdge(selectedEdge);
|
|
20
|
+
}
|
|
21
|
+
if (e.key === "Escape") {
|
|
22
|
+
setConnecting(null);
|
|
23
|
+
setSelectedEdge(null);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
27
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
28
|
+
}, [selectedEdge]);
|
|
29
|
+
// Convert client coords → SVG coords
|
|
30
|
+
const getSVGPoint = useCallback((clientX, clientY) => {
|
|
31
|
+
var _a;
|
|
32
|
+
const svg = svgRef.current;
|
|
33
|
+
if (!svg)
|
|
34
|
+
return { x: 0, y: 0 };
|
|
35
|
+
const pt = svg.createSVGPoint();
|
|
36
|
+
pt.x = clientX;
|
|
37
|
+
pt.y = clientY;
|
|
38
|
+
const svgP = pt.matrixTransform((_a = svg.getScreenCTM()) === null || _a === void 0 ? void 0 : _a.inverse());
|
|
39
|
+
return { x: svgP.x, y: svgP.y };
|
|
40
|
+
}, [svgRef]);
|
|
41
|
+
// ── Mouse move ───────────────────────────────────────────────────
|
|
42
|
+
const onMouseMove = useCallback((e) => {
|
|
43
|
+
const clientX = "clientX" in e ? e.clientX : 0;
|
|
44
|
+
const clientY = "clientY" in e ? e.clientY : 0;
|
|
45
|
+
setMousePos(getSVGPoint(clientX, clientY));
|
|
46
|
+
}, [getSVGPoint]);
|
|
47
|
+
// ── Connect ──────────────────────────────────────────────────────
|
|
48
|
+
const startConnect = useCallback((fromId) => {
|
|
49
|
+
setConnecting({ fromId });
|
|
50
|
+
setSelectedEdge(null);
|
|
51
|
+
}, []);
|
|
52
|
+
const endConnect = useCallback((toId) => {
|
|
53
|
+
if (!connecting || connecting.fromId === toId) {
|
|
54
|
+
setConnecting(null);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const fromNode = getNodeById(connecting.fromId);
|
|
58
|
+
const toNode = getNodeById(toId);
|
|
59
|
+
if (!fromNode || !toNode) {
|
|
60
|
+
setConnecting(null);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setEdges((prev) => {
|
|
64
|
+
const exists = prev.find((e) => (e.from === connecting.fromId && e.to === toId) ||
|
|
65
|
+
(e.from === toId && e.to === connecting.fromId));
|
|
66
|
+
if (exists)
|
|
67
|
+
return prev;
|
|
68
|
+
return [
|
|
69
|
+
...prev,
|
|
70
|
+
{ id: `edge-${Date.now()}`, from: connecting.fromId, to: toId },
|
|
71
|
+
];
|
|
72
|
+
});
|
|
73
|
+
setConnecting(null);
|
|
74
|
+
}, [connecting, getNodeById]);
|
|
75
|
+
const cancelConnect = useCallback(() => setConnecting(null), []);
|
|
76
|
+
// ── Edge select / delete ─────────────────────────────────────────
|
|
77
|
+
const selectEdge = useCallback((edgeId) => {
|
|
78
|
+
setSelectedEdge(edgeId);
|
|
79
|
+
}, []);
|
|
80
|
+
const deleteEdge = useCallback((edgeId) => {
|
|
81
|
+
setEdges((prev) => prev.filter((e) => e.id !== edgeId));
|
|
82
|
+
setSelectedEdge(null);
|
|
83
|
+
}, []);
|
|
84
|
+
const clearSelection = useCallback(() => {
|
|
85
|
+
setSelectedEdge(null);
|
|
86
|
+
}, []);
|
|
87
|
+
return {
|
|
88
|
+
edges,
|
|
89
|
+
setEdges,
|
|
90
|
+
selectedEdge,
|
|
91
|
+
connecting,
|
|
92
|
+
mousePos,
|
|
93
|
+
// actions
|
|
94
|
+
startConnect,
|
|
95
|
+
endConnect,
|
|
96
|
+
cancelConnect,
|
|
97
|
+
selectEdge,
|
|
98
|
+
deleteEdge,
|
|
99
|
+
clearSelection,
|
|
100
|
+
// svg handlers
|
|
101
|
+
onMouseMove,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export declare const GRID_SIZE = 20;
|
|
2
|
+
export declare const NODE_WIDTH = 120;
|
|
3
|
+
export declare const NODE_HEIGHT = 50;
|
|
4
|
+
export declare const snap: (v: number) => number;
|
|
5
|
+
export type EdgeType = {
|
|
6
|
+
id: string;
|
|
7
|
+
from: string;
|
|
8
|
+
to: string;
|
|
9
|
+
};
|
|
10
|
+
export type NodeType = {
|
|
11
|
+
id: string;
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width?: number;
|
|
15
|
+
height?: number;
|
|
16
|
+
rotation?: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Rotate point (px, py) sekitar origin (0,0) sebesar deg derajat.
|
|
20
|
+
*/
|
|
21
|
+
export declare const rotatePoint: (px: number, py: number, deg: number) => {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Hitung titik edge pada sisi rect node yang menghadap ke arah `to`,
|
|
27
|
+
* dengan mempertimbangkan rotation node.
|
|
28
|
+
*/
|
|
29
|
+
export declare const getRectEdge: (from: {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
width?: number;
|
|
33
|
+
height?: number;
|
|
34
|
+
rotation?: number;
|
|
35
|
+
}, to: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
}, width: number, height: number) => {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Build cubic bezier path dari start ke end.
|
|
44
|
+
* Control points dihitung otomatis berdasarkan arah dominan.
|
|
45
|
+
*
|
|
46
|
+
* Horizontal dominan: CP horizontal (kiri-kanan)
|
|
47
|
+
* Vertikal dominan: CP vertikal (atas-bawah)
|
|
48
|
+
*/
|
|
49
|
+
export declare const buildCurvePath: (start: {
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
}, end: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
}) => string;
|
|
56
|
+
/**
|
|
57
|
+
* Hitung arah arrow dari CP2 → end point
|
|
58
|
+
* agar panah sejajar dengan ujung kurva.
|
|
59
|
+
*/
|
|
60
|
+
export declare const getCurveEndAngle: (start: {
|
|
61
|
+
x: number;
|
|
62
|
+
y: number;
|
|
63
|
+
}, end: {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
}) => number;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// utils.ts
|
|
2
|
+
export const GRID_SIZE = 20;
|
|
3
|
+
export const NODE_WIDTH = 120;
|
|
4
|
+
export const NODE_HEIGHT = 50;
|
|
5
|
+
export const snap = (v) => Math.round(v / GRID_SIZE) * GRID_SIZE;
|
|
6
|
+
/**
|
|
7
|
+
* Rotate point (px, py) sekitar origin (0,0) sebesar deg derajat.
|
|
8
|
+
*/
|
|
9
|
+
export const rotatePoint = (px, py, deg) => {
|
|
10
|
+
const rad = (deg * Math.PI) / 180;
|
|
11
|
+
return {
|
|
12
|
+
x: px * Math.cos(rad) - py * Math.sin(rad),
|
|
13
|
+
y: px * Math.sin(rad) + py * Math.cos(rad),
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Hitung titik edge pada sisi rect node yang menghadap ke arah `to`,
|
|
18
|
+
* dengan mempertimbangkan rotation node.
|
|
19
|
+
*/
|
|
20
|
+
export const getRectEdge = (from, to, width, height) => {
|
|
21
|
+
var _a, _b, _c;
|
|
22
|
+
const w = (_a = from.width) !== null && _a !== void 0 ? _a : NODE_WIDTH;
|
|
23
|
+
const h = (_b = from.height) !== null && _b !== void 0 ? _b : NODE_HEIGHT;
|
|
24
|
+
const rotation = (_c = from.rotation) !== null && _c !== void 0 ? _c : 0;
|
|
25
|
+
const dx = to.x - from.x;
|
|
26
|
+
const dy = to.y - from.y;
|
|
27
|
+
if (dx === 0 && dy === 0)
|
|
28
|
+
return { x: from.x, y: from.y };
|
|
29
|
+
const angle = Math.atan2(dy, dx);
|
|
30
|
+
// Un-rotate arah ke local space node
|
|
31
|
+
const rad = -(rotation * Math.PI) / 180;
|
|
32
|
+
const localAngle = angle + rad;
|
|
33
|
+
const absCos = Math.abs(Math.cos(localAngle));
|
|
34
|
+
const absSin = Math.abs(Math.sin(localAngle));
|
|
35
|
+
const offset = (h / 2) * absCos <= (w / 2) * absSin
|
|
36
|
+
? (h / 2) / absSin
|
|
37
|
+
: (w / 2) / absCos;
|
|
38
|
+
const localEdgeX = Math.cos(localAngle) * offset;
|
|
39
|
+
const localEdgeY = Math.sin(localAngle) * offset;
|
|
40
|
+
// Re-rotate balik ke world space
|
|
41
|
+
const worldEdge = rotatePoint(localEdgeX, localEdgeY, rotation);
|
|
42
|
+
return {
|
|
43
|
+
x: snap(from.x + worldEdge.x),
|
|
44
|
+
y: snap(from.y + worldEdge.y),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Build cubic bezier path dari start ke end.
|
|
49
|
+
* Control points dihitung otomatis berdasarkan arah dominan.
|
|
50
|
+
*
|
|
51
|
+
* Horizontal dominan: CP horizontal (kiri-kanan)
|
|
52
|
+
* Vertikal dominan: CP vertikal (atas-bawah)
|
|
53
|
+
*/
|
|
54
|
+
export const buildCurvePath = (start, end) => {
|
|
55
|
+
const dx = end.x - start.x;
|
|
56
|
+
const dy = end.y - start.y;
|
|
57
|
+
const absDx = Math.abs(dx);
|
|
58
|
+
const absDy = Math.abs(dy);
|
|
59
|
+
// Kekuatan lengkungan — makin jauh makin melengkung, min 60px
|
|
60
|
+
const strength = Math.max(Math.max(absDx, absDy) * 0.5, 60);
|
|
61
|
+
let cp1;
|
|
62
|
+
let cp2;
|
|
63
|
+
if (absDx >= absDy) {
|
|
64
|
+
// Dominan horizontal → CP keluar ke kiri/kanan
|
|
65
|
+
cp1 = { x: start.x + strength, y: start.y };
|
|
66
|
+
cp2 = { x: end.x - strength, y: end.y };
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Dominan vertikal → CP keluar ke atas/bawah
|
|
70
|
+
cp1 = { x: start.x, y: start.y + strength };
|
|
71
|
+
cp2 = { x: end.x, y: end.y - strength };
|
|
72
|
+
}
|
|
73
|
+
return `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Hitung arah arrow dari CP2 → end point
|
|
77
|
+
* agar panah sejajar dengan ujung kurva.
|
|
78
|
+
*/
|
|
79
|
+
export const getCurveEndAngle = (start, end) => {
|
|
80
|
+
const dx = end.x - start.x;
|
|
81
|
+
const dy = end.y - start.y;
|
|
82
|
+
const absDx = Math.abs(dx);
|
|
83
|
+
const absDy = Math.abs(dy);
|
|
84
|
+
const strength = Math.max(Math.max(absDx, absDy) * 0.5, 60);
|
|
85
|
+
let cp2;
|
|
86
|
+
if (absDx >= absDy) {
|
|
87
|
+
cp2 = { x: end.x - strength, y: end.y };
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
cp2 = { x: end.x, y: end.y - strength };
|
|
91
|
+
}
|
|
92
|
+
// Sudut dari CP2 ke end
|
|
93
|
+
const adx = end.x - cp2.x;
|
|
94
|
+
const ady = end.y - cp2.y;
|
|
95
|
+
return (Math.atan2(ady, adx) * 180) / Math.PI;
|
|
96
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
cx: number;
|
|
3
|
+
cy: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
nodeId: string;
|
|
7
|
+
rotation?: number;
|
|
8
|
+
isConnecting: boolean;
|
|
9
|
+
onStartConnect: (nodeId: string) => void;
|
|
10
|
+
onEndConnect: (nodeId: string) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare const ConnectHandle: React.FC<Props>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const ConnectHandle = ({ cx, cy, width, height, nodeId, rotation = 0, // ← tambah ini
|
|
3
|
+
isConnecting, onStartConnect, onEndConnect, }) => {
|
|
4
|
+
const hw = width / 2;
|
|
5
|
+
const hh = height / 2;
|
|
6
|
+
// Tombol ⊕ di pojok kanan atas (dalam local space)
|
|
7
|
+
const btnLocalX = hw - 2;
|
|
8
|
+
const btnLocalY = -hh - 8;
|
|
9
|
+
return (
|
|
10
|
+
// Semua di-wrap dalam g dengan translate ke center + rotate
|
|
11
|
+
// Sama persis seperti struktur render node aslinya
|
|
12
|
+
_jsx("g", { transform: `translate(${cx}, ${cy}) rotate(${rotation})`, children: isConnecting ? (
|
|
13
|
+
// Mode connecting: overlay seluruh node
|
|
14
|
+
_jsx("rect", { x: -hw - 4, y: -hh - 4, width: width + 8, height: height + 8, fill: "transparent", stroke: "#a78bfa", strokeWidth: 1.5, strokeDasharray: "4 2", rx: 4, style: { cursor: "crosshair" }, onMouseDown: (e) => {
|
|
15
|
+
e.stopPropagation();
|
|
16
|
+
onEndConnect(nodeId);
|
|
17
|
+
} })) : (
|
|
18
|
+
// Tombol ⊕ — posisi relatif dari center node
|
|
19
|
+
_jsxs("g", { transform: `translate(${btnLocalX}, ${btnLocalY})`, style: { cursor: "crosshair" }, onMouseDown: (e) => {
|
|
20
|
+
e.stopPropagation();
|
|
21
|
+
onStartConnect(nodeId);
|
|
22
|
+
}, children: [_jsx("circle", { r: 8, fill: "#0a0e1a", stroke: "#38bdf8", strokeWidth: 1.2, opacity: 0.9 }), _jsx("text", { textAnchor: "middle", dominantBaseline: "middle", fill: "#38bdf8", fontSize: 11, fontWeight: 700, style: { pointerEvents: "none", userSelect: "none" }, children: "\u2295" })] })) }));
|
|
23
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const ConnectHandle = ({ cx, cy, width, height, nodeId, rotation = 0, // ← tambah ini
|
|
2
|
+
isConnecting, onStartConnect, onEndConnect, }) => {
|
|
3
|
+
const hw = width / 2;
|
|
4
|
+
const hh = height / 2;
|
|
5
|
+
// Tombol ⊕ di pojok kanan atas (dalam local space)
|
|
6
|
+
const btnLocalX = hw - 2;
|
|
7
|
+
const btnLocalY = -hh - 8;
|
|
8
|
+
return (
|
|
9
|
+
// Semua di-wrap dalam g dengan translate ke center + rotate
|
|
10
|
+
// Sama persis seperti struktur render node aslinya
|
|
11
|
+
<g transform={`translate(${cx}, ${cy}) rotate(${rotation})`}>
|
|
12
|
+
|
|
13
|
+
{isConnecting ? (
|
|
14
|
+
// Mode connecting: overlay seluruh node
|
|
15
|
+
<rect x={-hw - 4} y={-hh - 4} width={width + 8} height={height + 8} fill="transparent" stroke="#a78bfa" strokeWidth={1.5} strokeDasharray="4 2" rx={4} style={{ cursor: "crosshair" }} onMouseDown={(e) => {
|
|
16
|
+
e.stopPropagation();
|
|
17
|
+
onEndConnect(nodeId);
|
|
18
|
+
}}/>) : (
|
|
19
|
+
// Tombol ⊕ — posisi relatif dari center node
|
|
20
|
+
<g transform={`translate(${btnLocalX}, ${btnLocalY})`} style={{ cursor: "crosshair" }} onMouseDown={(e) => {
|
|
21
|
+
e.stopPropagation();
|
|
22
|
+
onStartConnect(nodeId);
|
|
23
|
+
}}>
|
|
24
|
+
<circle r={8} fill="#0a0e1a" stroke="#38bdf8" strokeWidth={1.2} opacity={0.9}/>
|
|
25
|
+
<text textAnchor="middle" dominantBaseline="middle" fill="#38bdf8" fontSize={11} fontWeight={700} style={{ pointerEvents: "none", userSelect: "none" }}>
|
|
26
|
+
⊕
|
|
27
|
+
</text>
|
|
28
|
+
</g>)}
|
|
29
|
+
</g>);
|
|
30
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { NodeType } from "./utils";
|
|
3
|
+
export type EdgeType = {
|
|
4
|
+
id: string;
|
|
5
|
+
from: string;
|
|
6
|
+
to: string;
|
|
7
|
+
};
|
|
8
|
+
type Props = {
|
|
9
|
+
edges: EdgeType[];
|
|
10
|
+
selectedEdge: string | null;
|
|
11
|
+
connecting: {
|
|
12
|
+
fromId: string;
|
|
13
|
+
} | null;
|
|
14
|
+
mousePos: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
getNodeById: (id: string) => NodeType | null;
|
|
19
|
+
onSelectEdge: (edgeId: string) => void;
|
|
20
|
+
};
|
|
21
|
+
export declare const ConnectionLayer: React.FC<Props>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// components/ConnectionLayer.tsx
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { buildCurvePath, getCurveEndAngle, getRectEdge, NODE_WIDTH, NODE_HEIGHT, } from "./utils";
|
|
5
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
6
|
+
const LEVEL_COLORS = [
|
|
7
|
+
"#22c55e",
|
|
8
|
+
"#f59e0b",
|
|
9
|
+
"#f97316",
|
|
10
|
+
"#ef4444",
|
|
11
|
+
"#a78bfa",
|
|
12
|
+
"#38bdf8",
|
|
13
|
+
];
|
|
14
|
+
const getLevelColor = (level) => LEVEL_COLORS[Math.min(level, LEVEL_COLORS.length - 1)];
|
|
15
|
+
// ─── BFS flood fill ───────────────────────────────────────────────────────────
|
|
16
|
+
const floodFill = (seedIds, edges) => {
|
|
17
|
+
const levelMap = new Map();
|
|
18
|
+
const queue = [];
|
|
19
|
+
seedIds.forEach((id) => { levelMap.set(id, 0); queue.push({ id, level: 0 }); });
|
|
20
|
+
while (queue.length > 0) {
|
|
21
|
+
const { id, level } = queue.shift();
|
|
22
|
+
edges.forEach((edge) => {
|
|
23
|
+
const neighbors = [];
|
|
24
|
+
if (edge.from === id)
|
|
25
|
+
neighbors.push(edge.to);
|
|
26
|
+
if (edge.to === id)
|
|
27
|
+
neighbors.push(edge.from);
|
|
28
|
+
neighbors.forEach((nId) => {
|
|
29
|
+
if (!levelMap.has(nId)) {
|
|
30
|
+
levelMap.set(nId, level + 1);
|
|
31
|
+
queue.push({ id: nId, level: level + 1 });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return levelMap;
|
|
37
|
+
};
|
|
38
|
+
// ─── Group edges by source ────────────────────────────────────────────────────
|
|
39
|
+
const groupBySource = (edges) => {
|
|
40
|
+
const map = new Map();
|
|
41
|
+
edges.forEach((edge) => {
|
|
42
|
+
if (!map.has(edge.from))
|
|
43
|
+
map.set(edge.from, []);
|
|
44
|
+
map.get(edge.from).push(edge);
|
|
45
|
+
});
|
|
46
|
+
return map;
|
|
47
|
+
};
|
|
48
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
49
|
+
export const ConnectionLayer = ({ edges, selectedEdge, connecting, mousePos, getNodeById, onSelectEdge,
|
|
50
|
+
// onDeleteEdge,
|
|
51
|
+
}) => {
|
|
52
|
+
const [hoveredEdge, setHoveredEdge] = useState(null);
|
|
53
|
+
// ── Flood fill highlight ──────────────────────────────────────────
|
|
54
|
+
const nodeHighlightMap = useMemo(() => {
|
|
55
|
+
if (!selectedEdge)
|
|
56
|
+
return new Map();
|
|
57
|
+
const sel = edges.find((e) => e.id === selectedEdge);
|
|
58
|
+
if (!sel)
|
|
59
|
+
return new Map();
|
|
60
|
+
return floodFill([sel.from, sel.to], edges);
|
|
61
|
+
}, [selectedEdge, edges]);
|
|
62
|
+
// ── Group by source for shared stem ──────────────────────────────
|
|
63
|
+
const sourceGroups = useMemo(() => groupBySource(edges), [edges]);
|
|
64
|
+
// ── Pre-compute curve data per edge ──────────────────────────────
|
|
65
|
+
const curveData = useMemo(() => {
|
|
66
|
+
const map = new Map();
|
|
67
|
+
edges.forEach((edge) => {
|
|
68
|
+
const fromNode = getNodeById(edge.from);
|
|
69
|
+
const toNode = getNodeById(edge.to);
|
|
70
|
+
if (!fromNode || !toNode)
|
|
71
|
+
return;
|
|
72
|
+
const start = getRectEdge(fromNode, toNode, fromNode.width, fromNode.height);
|
|
73
|
+
const end = getRectEdge(toNode, fromNode, toNode.width, toNode.height);
|
|
74
|
+
const pathD = buildCurvePath(start, end);
|
|
75
|
+
const angle = getCurveEndAngle(start, end);
|
|
76
|
+
// Midpoint arc/elbow → titik belok pertama (elbow kiri/atas)
|
|
77
|
+
const dx = end.x - start.x;
|
|
78
|
+
const dy = end.y - start.y;
|
|
79
|
+
const absDx = Math.abs(dx);
|
|
80
|
+
const absDy = Math.abs(dy);
|
|
81
|
+
let midX;
|
|
82
|
+
let midY;
|
|
83
|
+
if (absDx >= absDy) {
|
|
84
|
+
// Dominan horizontal → elbow di (midX, start.y)
|
|
85
|
+
midX = (start.x + end.x) / 2;
|
|
86
|
+
midY = (start.y + end.y) / 2;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Dominan vertikal → elbow di (start.x, midY)
|
|
90
|
+
midX = (start.x + end.x) / 2;
|
|
91
|
+
midY = (start.y + end.y) / 2;
|
|
92
|
+
}
|
|
93
|
+
map.set(edge.id, { start, end, pathD, angle, midX, midY });
|
|
94
|
+
});
|
|
95
|
+
return map;
|
|
96
|
+
}, [edges, getNodeById]);
|
|
97
|
+
// ── Shared stem exit point per source ────────────────────────────
|
|
98
|
+
const stemData = useMemo(() => {
|
|
99
|
+
const map = new Map();
|
|
100
|
+
sourceGroups.forEach((groupEdges, fromId) => {
|
|
101
|
+
if (groupEdges.length <= 1)
|
|
102
|
+
return;
|
|
103
|
+
const sourceNode = getNodeById(fromId);
|
|
104
|
+
if (!sourceNode)
|
|
105
|
+
return;
|
|
106
|
+
const exits = groupEdges
|
|
107
|
+
.map((edge) => {
|
|
108
|
+
const toNode = getNodeById(edge.to);
|
|
109
|
+
if (!toNode)
|
|
110
|
+
return null;
|
|
111
|
+
return getRectEdge(sourceNode, toNode, sourceNode.width, sourceNode.height);
|
|
112
|
+
})
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
if (exits.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
map.set(fromId, {
|
|
117
|
+
x: exits.reduce((s, p) => s + p.x, 0) / exits.length,
|
|
118
|
+
y: exits.reduce((s, p) => s + p.y, 0) / exits.length,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
return map;
|
|
122
|
+
}, [sourceGroups, getNodeById]);
|
|
123
|
+
return (_jsxs("g", { id: "connection-layer", children: [Array.from(nodeHighlightMap.entries()).map(([nodeId, level]) => {
|
|
124
|
+
var _a, _b, _c;
|
|
125
|
+
const node = getNodeById(nodeId);
|
|
126
|
+
if (!node)
|
|
127
|
+
return null;
|
|
128
|
+
const w = (_a = node.width) !== null && _a !== void 0 ? _a : NODE_WIDTH;
|
|
129
|
+
const h = (_b = node.height) !== null && _b !== void 0 ? _b : NODE_HEIGHT;
|
|
130
|
+
const rotation = (_c = node.rotation) !== null && _c !== void 0 ? _c : 0;
|
|
131
|
+
const color = getLevelColor(level);
|
|
132
|
+
const padding = 4;
|
|
133
|
+
return (_jsxs("g", { transform: `translate(${node.x}, ${node.y}) rotate(${rotation})`, style: { pointerEvents: "none" }, children: [_jsx("rect", { x: -w / 2 - padding, y: -h / 2 - padding, width: w + padding * 2, height: h + padding * 2, fill: "none", stroke: color, strokeWidth: 8, rx: 5, opacity: 0.08 }), _jsx("rect", { x: -w / 2 - padding, y: -h / 2 - padding, width: w + padding * 2, height: h + padding * 2, fill: "none", stroke: color, strokeWidth: level === 0 ? 2.5 : 1.5, strokeDasharray: level === 0 ? "none" : "5 3", rx: 5, opacity: level === 0 ? 1 : 0.75 }), _jsxs("g", { transform: `translate(${w / 2 + padding - 2}, ${-h / 2 - padding})`, children: [_jsx("circle", { r: 7, fill: color, opacity: 0.9 }), _jsx("text", { textAnchor: "middle", dominantBaseline: "middle", fill: "white", fontSize: 7, fontFamily: "monospace", fontWeight: 700, style: { userSelect: "none" }, children: level })] })] }, `highlight-${nodeId}`));
|
|
134
|
+
}), Array.from(stemData.entries()).map(([fromId, stemExit]) => {
|
|
135
|
+
var _a;
|
|
136
|
+
const isStemOfSelected = selectedEdge && ((_a = edges.find((e) => e.id === selectedEdge)) === null || _a === void 0 ? void 0 : _a.from) === fromId;
|
|
137
|
+
const color = isStemOfSelected ? "#a78bfa" : "#38bdf8";
|
|
138
|
+
return (_jsxs("g", { style: { pointerEvents: "none" }, children: [_jsx("circle", { cx: stemExit.x, cy: stemExit.y, r: 5, fill: "#0f172a", stroke: color, strokeWidth: 1.5 }), _jsx("circle", { cx: stemExit.x, cy: stemExit.y, r: 2.5, fill: color })] }, `stem-dot-${fromId}`));
|
|
139
|
+
}), edges.map((edge) => {
|
|
140
|
+
const data = curveData.get(edge.id);
|
|
141
|
+
if (!data)
|
|
142
|
+
return null;
|
|
143
|
+
const { pathD, angle, midX, midY } = data;
|
|
144
|
+
const isSel = selectedEdge === edge.id;
|
|
145
|
+
const isHov = hoveredEdge === edge.id;
|
|
146
|
+
const markerId = `conn-arrow-${edge.id}`;
|
|
147
|
+
const markerSelId = `conn-arrow-sel-${edge.id}`;
|
|
148
|
+
// Warna edge jika dalam network highlight
|
|
149
|
+
const fromLevel = nodeHighlightMap.get(edge.from);
|
|
150
|
+
const toLevel = nodeHighlightMap.get(edge.to);
|
|
151
|
+
const isInNetwork = fromLevel !== undefined && toLevel !== undefined;
|
|
152
|
+
const networkColor = isInNetwork
|
|
153
|
+
? getLevelColor(Math.max(fromLevel, toLevel))
|
|
154
|
+
: "#38bdf8";
|
|
155
|
+
return (_jsxs("g", { children: [_jsx("defs", { children: _jsx("marker", { id: markerId, markerWidth: "8", markerHeight: "8", refX: "7", refY: "3", orient: `${angle}deg`, children: _jsx("path", { d: "M 0 0 L 8 3 L 0 6 Z", fill: isSel ? "#a78bfa" : isInNetwork && !isSel ? networkColor : "#38bdf8", opacity: 0.9 }) }) }), _jsx("path", { d: pathD, stroke: "transparent", strokeWidth: 14, fill: "none", style: { cursor: "pointer" }, onClick: (e) => { e.stopPropagation(); onSelectEdge(edge.id); }, onMouseEnter: () => setHoveredEdge(edge.id), onMouseLeave: () => setHoveredEdge(null) }), (isSel || isHov) && (_jsx("path", { d: pathD, stroke: isSel ? "#a78bfa" : "#38bdf8", strokeWidth: 8, fill: "none", opacity: isSel ? 0.12 : 0.06, style: { pointerEvents: "none" } })), _jsx("path", { d: pathD, stroke: isSel ? "#a78bfa" : isInNetwork && selectedEdge ? networkColor : "#38bdf8", strokeWidth: isSel ? 2 : isHov ? 2 : 1.5, fill: "none", opacity: selectedEdge && !isSel && !isInNetwork ? 0.25 : 0.9, markerEnd: `url(#${markerId})`, style: { pointerEvents: "none" } }), isSel && (_jsxs("g", { style: { pointerEvents: "none" }, children: [_jsx("rect", { x: midX - 16, y: midY - 10, width: 32, height: 18, rx: 4, fill: "#1e1b2e", stroke: "#a78bfa", strokeWidth: 1, opacity: 0.95 }), _jsx("text", { x: midX, y: midY, textAnchor: "middle", dominantBaseline: "middle", fill: "#a78bfa", fontSize: 9, fontFamily: "monospace", fontWeight: 600, children: "DEL" })] }))] }, edge.id));
|
|
156
|
+
}), connecting && (() => {
|
|
157
|
+
const fromNode = getNodeById(connecting.fromId);
|
|
158
|
+
if (!fromNode)
|
|
159
|
+
return null;
|
|
160
|
+
const start = getRectEdge(fromNode, mousePos, fromNode.width, fromNode.height);
|
|
161
|
+
const pathD = buildCurvePath(start, mousePos);
|
|
162
|
+
const angle = getCurveEndAngle(start, mousePos);
|
|
163
|
+
return (_jsxs("g", { children: [_jsx("defs", { children: _jsx("marker", { id: "conn-preview-arrow", markerWidth: "8", markerHeight: "8", refX: "7", refY: "3", orient: `${angle}deg`, children: _jsx("path", { d: "M 0 0 L 8 3 L 0 6 Z", fill: "#a78bfa", opacity: 0.85 }) }) }), _jsx("path", { d: pathD, stroke: "#a78bfa", strokeWidth: 1.5, fill: "none", strokeDasharray: "6 3", markerEnd: "url(#conn-preview-arrow)", opacity: 0.8, style: { pointerEvents: "none" } })] }));
|
|
164
|
+
})()] }));
|
|
165
|
+
};
|