seat-editor 3.4.7 → 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/new-board/page.js +43 -7
- package/dist/app/new-board/page.jsx +45 -12
- package/dist/components/button-tools/index.js +7 -5
- package/dist/components/button-tools/index.jsx +21 -9
- package/dist/components/form-tools/label.js +9 -20
- package/dist/components/form-tools/label.jsx +38 -28
- package/dist/components/form-tools/shape.js +5 -5
- package/dist/components/form-tools/shape.jsx +8 -8
- package/dist/components/layer-v3/index.js +44 -3
- package/dist/components/layer-v3/index.jsx +120 -3
- package/dist/components/layer-v4/index.js +3 -2
- package/dist/components/layer-v4/index.jsx +3 -2
- 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/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/package/index.d.ts +2 -0
- package/dist/features/package/index.js +1 -1
- package/dist/features/package/index.jsx +6 -1
- package/dist/features/panel/index.d.ts +8 -0
- 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.js +3 -0
- package/dist/features/panel/select-tool.jsx +3 -0
- package/dist/features/panel/selected-group.js +24 -26
- package/dist/features/panel/selected-group.jsx +56 -51
- package/dist/features/panel/text-tool.js +17 -2
- package/dist/features/panel/text-tool.jsx +19 -2
- package/dist/features/panel/upload-tool.js +17 -3
- package/dist/features/panel/upload-tool.jsx +23 -4
- package/dist/features/side-tool/index.js +43 -6
- package/dist/features/side-tool/index.jsx +47 -10
- 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/seat-editor.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// use-connection-graph.ts
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { snap } from "./utils";
|
|
4
|
+
import { useAppSelector } from "@/hooks/use-redux";
|
|
5
|
+
export const useConnectionGraph = ({ svgRef, getNodeById, onEdgesChange, keyNode, mappingKey, tableMatchKey, statusKey, }) => {
|
|
6
|
+
const components = useAppSelector((state) => state.board.components);
|
|
7
|
+
const [edges, setEdges] = useState([]);
|
|
8
|
+
const [selectedEdge, setSelectedEdge] = useState(null);
|
|
9
|
+
const [selectedNode, setSelectedNode] = useState(null);
|
|
10
|
+
const [connecting, setConnecting] = useState(null);
|
|
11
|
+
const [draggingAnchor, setDraggingAnchor] = useState(null);
|
|
12
|
+
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
13
|
+
const draggingWP = useRef(null);
|
|
14
|
+
const getSVGPoint = useCallback((clientX, clientY) => {
|
|
15
|
+
var _a;
|
|
16
|
+
const svg = svgRef.current;
|
|
17
|
+
if (!svg)
|
|
18
|
+
return { x: 0, y: 0 };
|
|
19
|
+
const pt = svg.createSVGPoint();
|
|
20
|
+
pt.x = clientX;
|
|
21
|
+
pt.y = clientY;
|
|
22
|
+
const p = pt.matrixTransform((_a = svg.getScreenCTM()) === null || _a === void 0 ? void 0 : _a.inverse());
|
|
23
|
+
return { x: p.x, y: p.y };
|
|
24
|
+
}, [svgRef]);
|
|
25
|
+
// ── Connect ──────────────────────────────────────────────────────
|
|
26
|
+
const startConnect = useCallback((fromId, fromPos) => {
|
|
27
|
+
setConnecting({ fromId, fromPos });
|
|
28
|
+
setSelectedEdge(null);
|
|
29
|
+
}, []);
|
|
30
|
+
const endConnect = useCallback((toId, toPos) => {
|
|
31
|
+
if (!connecting || connecting.fromId === toId) {
|
|
32
|
+
setConnecting(null);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setEdges((prev) => {
|
|
36
|
+
const exists = prev.find((e) => (e.from === connecting.fromId && e.to === toId) ||
|
|
37
|
+
(e.from === toId && e.to === connecting.fromId));
|
|
38
|
+
if (exists)
|
|
39
|
+
return prev;
|
|
40
|
+
return [
|
|
41
|
+
...prev,
|
|
42
|
+
{
|
|
43
|
+
id: `edge-${Date.now()}`,
|
|
44
|
+
from: connecting.fromId,
|
|
45
|
+
to: toId,
|
|
46
|
+
fromPos: connecting.fromPos,
|
|
47
|
+
toPos,
|
|
48
|
+
waypoints: [],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
});
|
|
52
|
+
setConnecting(null);
|
|
53
|
+
}, [connecting]);
|
|
54
|
+
const cancelConnect = useCallback(() => setConnecting(null), []);
|
|
55
|
+
// ── Drag endpoint ────────────────────────────────────────────────
|
|
56
|
+
const startDragAnchor = useCallback((e, edgeId, side) => {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
setDraggingAnchor({ edgeId, side });
|
|
59
|
+
}, []);
|
|
60
|
+
const updateAnchor = useCallback((targetNodeId, newPos) => {
|
|
61
|
+
if (!draggingAnchor)
|
|
62
|
+
return;
|
|
63
|
+
const { edgeId, side } = draggingAnchor;
|
|
64
|
+
setEdges((prev) => prev.map((edge) => {
|
|
65
|
+
if (edge.id !== edgeId)
|
|
66
|
+
return edge;
|
|
67
|
+
if (side === "from") {
|
|
68
|
+
if (targetNodeId === edge.to)
|
|
69
|
+
return edge;
|
|
70
|
+
return Object.assign(Object.assign({}, edge), { from: targetNodeId, fromPos: newPos, waypoints: [] });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
if (targetNodeId === edge.from)
|
|
74
|
+
return edge;
|
|
75
|
+
return Object.assign(Object.assign({}, edge), { to: targetNodeId, toPos: newPos, waypoints: [] });
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
setDraggingAnchor(null);
|
|
79
|
+
}, [draggingAnchor]);
|
|
80
|
+
const cancelDragAnchor = useCallback(() => setDraggingAnchor(null), []);
|
|
81
|
+
// ── Mouse ────────────────────────────────────────────────────────
|
|
82
|
+
const onMouseMove = useCallback((e) => {
|
|
83
|
+
const pos = getSVGPoint(e.clientX, e.clientY);
|
|
84
|
+
setMousePos(pos);
|
|
85
|
+
if (draggingWP.current) {
|
|
86
|
+
const { edgeId, index } = draggingWP.current;
|
|
87
|
+
setEdges((prev) => prev.map((ed) => {
|
|
88
|
+
if (ed.id !== edgeId)
|
|
89
|
+
return ed;
|
|
90
|
+
return Object.assign(Object.assign({}, ed), { waypoints: ed.waypoints.map((wp, i) => i === index ? { x: snap(pos.x), y: snap(pos.y) } : wp) });
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}, [getSVGPoint]);
|
|
94
|
+
const onMouseUp = useCallback(() => {
|
|
95
|
+
draggingWP.current = null;
|
|
96
|
+
if (draggingAnchor)
|
|
97
|
+
setDraggingAnchor(null);
|
|
98
|
+
}, [draggingAnchor]);
|
|
99
|
+
// ── Waypoints ────────────────────────────────────────────────────
|
|
100
|
+
const startDragWaypoint = useCallback((e, edgeId, index) => {
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
draggingWP.current = { edgeId, index };
|
|
103
|
+
setSelectedEdge(edgeId);
|
|
104
|
+
}, []);
|
|
105
|
+
const insertWaypoint = useCallback((e, edgeId, insertIndex, x, y) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
setEdges((prev) => prev.map((ed) => {
|
|
108
|
+
if (ed.id !== edgeId)
|
|
109
|
+
return ed;
|
|
110
|
+
const newWP = [...ed.waypoints];
|
|
111
|
+
newWP.splice(insertIndex, 0, { x: snap(x), y: snap(y) });
|
|
112
|
+
return Object.assign(Object.assign({}, ed), { waypoints: newWP });
|
|
113
|
+
}));
|
|
114
|
+
draggingWP.current = { edgeId, index: insertIndex };
|
|
115
|
+
}, []);
|
|
116
|
+
const removeWaypoint = useCallback((e, edgeId, index) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
setEdges((prev) => prev.map((ed) => {
|
|
120
|
+
if (ed.id !== edgeId)
|
|
121
|
+
return ed;
|
|
122
|
+
if (ed.waypoints.length <= 1)
|
|
123
|
+
return ed;
|
|
124
|
+
return Object.assign(Object.assign({}, ed), { waypoints: ed.waypoints.filter((_, i) => i !== index) });
|
|
125
|
+
}));
|
|
126
|
+
}, []);
|
|
127
|
+
// __ Select Table
|
|
128
|
+
const selectNode = useCallback((nodeId) => setSelectedNode(nodeId), []);
|
|
129
|
+
// ── Select / delete ──────────────────────────────────────────────
|
|
130
|
+
const selectEdge = useCallback((id) => setSelectedEdge(id), []);
|
|
131
|
+
const deleteEdge = useCallback((id) => {
|
|
132
|
+
setEdges((p) => p.filter((e) => e.id !== id));
|
|
133
|
+
setSelectedEdge(null);
|
|
134
|
+
}, []);
|
|
135
|
+
const deleteSelectedEdge = useCallback(() => {
|
|
136
|
+
if (selectedEdge)
|
|
137
|
+
deleteEdge(selectedEdge);
|
|
138
|
+
}, [selectedEdge, deleteEdge]);
|
|
139
|
+
const clearSelection = useCallback(() => setSelectedEdge(null), []);
|
|
140
|
+
// ── Keyboard ─────────────────────────────────────────────────────
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const onKey = (e) => {
|
|
143
|
+
const tag = e.target.tagName.toLowerCase();
|
|
144
|
+
if (tag === "input" || tag === "textarea")
|
|
145
|
+
return;
|
|
146
|
+
if (e.key === "Delete" || e.key === "Backspace")
|
|
147
|
+
deleteSelectedEdge();
|
|
148
|
+
if (e.key === "Escape") {
|
|
149
|
+
cancelConnect();
|
|
150
|
+
cancelDragAnchor();
|
|
151
|
+
clearSelection();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
window.addEventListener("keydown", onKey);
|
|
155
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
156
|
+
}, [deleteSelectedEdge, cancelConnect, cancelDragAnchor, clearSelection]);
|
|
157
|
+
// ── onEdgesChange ────────────────────────────────────────────────
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const sourceToTargets = new Map();
|
|
160
|
+
edges.forEach((e) => {
|
|
161
|
+
if (!sourceToTargets.has(e.from))
|
|
162
|
+
sourceToTargets.set(e.from, []);
|
|
163
|
+
sourceToTargets.get(e.from).push(e.to);
|
|
164
|
+
});
|
|
165
|
+
const result = components === null || components === void 0 ? void 0 : components.map((table) => {
|
|
166
|
+
var _a, _b;
|
|
167
|
+
return (Object.assign(Object.assign({}, table), { [keyNode]: (_b = sourceToTargets.get(String((_a = table === null || table === void 0 ? void 0 : table[mappingKey]) === null || _a === void 0 ? void 0 : _a.id))) !== null && _b !== void 0 ? _b : [] }));
|
|
168
|
+
});
|
|
169
|
+
onEdgesChange === null || onEdgesChange === void 0 ? void 0 : onEdgesChange(edges, result);
|
|
170
|
+
}, [edges]);
|
|
171
|
+
return {
|
|
172
|
+
edges,
|
|
173
|
+
setEdges,
|
|
174
|
+
selectedEdge,
|
|
175
|
+
selectedNode,
|
|
176
|
+
connecting,
|
|
177
|
+
draggingAnchor,
|
|
178
|
+
mousePos,
|
|
179
|
+
startConnect,
|
|
180
|
+
endConnect,
|
|
181
|
+
cancelConnect,
|
|
182
|
+
startDragAnchor,
|
|
183
|
+
updateAnchor,
|
|
184
|
+
cancelDragAnchor,
|
|
185
|
+
startDragWaypoint,
|
|
186
|
+
insertWaypoint,
|
|
187
|
+
removeWaypoint,
|
|
188
|
+
selectEdge,
|
|
189
|
+
deleteEdge,
|
|
190
|
+
deleteSelectedEdge,
|
|
191
|
+
clearSelection,
|
|
192
|
+
onMouseMove,
|
|
193
|
+
onMouseUp,
|
|
194
|
+
selectNode
|
|
195
|
+
};
|
|
196
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ComponentProps, TableMatchKey } from ".";
|
|
2
|
+
export declare const GRID_SIZE = 20;
|
|
3
|
+
export declare const NODE_WIDTH = 120;
|
|
4
|
+
export declare const NODE_HEIGHT = 50;
|
|
5
|
+
export declare const snap: (v: number) => number;
|
|
6
|
+
export type EdgeType = {
|
|
7
|
+
id: string;
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
fromPos?: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
};
|
|
14
|
+
toPos?: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
waypoints: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
export type NodeType = {
|
|
24
|
+
id: string;
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width?: number;
|
|
28
|
+
height?: number;
|
|
29
|
+
rotation?: number;
|
|
30
|
+
};
|
|
31
|
+
export declare const rotatePoint: (px: number, py: number, deg: number) => {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
};
|
|
35
|
+
export declare const getRectEdge: (from: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
width?: number;
|
|
39
|
+
height?: number;
|
|
40
|
+
rotation?: number;
|
|
41
|
+
}, to: {
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
}, width?: number, height?: number) => {
|
|
45
|
+
x: number;
|
|
46
|
+
y: number;
|
|
47
|
+
};
|
|
48
|
+
export declare const buildPath: (points: {
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
}[]) => string;
|
|
52
|
+
export declare const renderElements: (elementEditor: ComponentProps[], mappingKey?: string, tableMatchKey?: TableMatchKey[], statusKey?: string) => ComponentProps[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const GRID_SIZE = 20;
|
|
2
|
+
export const NODE_WIDTH = 120;
|
|
3
|
+
export const NODE_HEIGHT = 50;
|
|
4
|
+
export const snap = (v) => Math.round(v / GRID_SIZE) * GRID_SIZE;
|
|
5
|
+
// ─── Rotate ──────────────────────────────────────────────────────────────────
|
|
6
|
+
export const rotatePoint = (px, py, deg) => {
|
|
7
|
+
const rad = (deg * Math.PI) / 180;
|
|
8
|
+
return {
|
|
9
|
+
x: px * Math.cos(rad) - py * Math.sin(rad),
|
|
10
|
+
y: px * Math.sin(rad) + py * Math.cos(rad),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
// ─── getRectEdge ─────────────────────────────────────────────────────────────
|
|
14
|
+
export const getRectEdge = (from, to, width, height) => {
|
|
15
|
+
var _a, _b, _c, _d, _e;
|
|
16
|
+
const w = (_b = (_a = from.width) !== null && _a !== void 0 ? _a : width) !== null && _b !== void 0 ? _b : NODE_WIDTH;
|
|
17
|
+
const h = (_d = (_c = from.height) !== null && _c !== void 0 ? _c : height) !== null && _d !== void 0 ? _d : NODE_HEIGHT;
|
|
18
|
+
const rotation = (_e = from.rotation) !== null && _e !== void 0 ? _e : 0;
|
|
19
|
+
const dx = to.x - from.x;
|
|
20
|
+
const dy = to.y - from.y;
|
|
21
|
+
if (dx === 0 && dy === 0)
|
|
22
|
+
return { x: from.x, y: from.y };
|
|
23
|
+
const angle = Math.atan2(dy, dx);
|
|
24
|
+
const rad = -(rotation * Math.PI) / 180;
|
|
25
|
+
const localAngle = angle + rad;
|
|
26
|
+
const absCos = Math.abs(Math.cos(localAngle));
|
|
27
|
+
const absSin = Math.abs(Math.sin(localAngle));
|
|
28
|
+
const offset = (h / 2) * absCos <= (w / 2) * absSin
|
|
29
|
+
? (h / 2) / absSin
|
|
30
|
+
: (w / 2) / absCos;
|
|
31
|
+
const localEdgeX = Math.cos(localAngle) * offset;
|
|
32
|
+
const localEdgeY = Math.sin(localAngle) * offset;
|
|
33
|
+
const worldEdge = rotatePoint(localEdgeX, localEdgeY, rotation);
|
|
34
|
+
return { x: from.x + worldEdge.x, y: from.y + worldEdge.y };
|
|
35
|
+
};
|
|
36
|
+
// ─── buildPath (single cubic bezier curve) ───────────────────────────────────
|
|
37
|
+
export const buildPath = (points) => {
|
|
38
|
+
if (points.length < 2)
|
|
39
|
+
return "";
|
|
40
|
+
const start = points[0];
|
|
41
|
+
const end = points[points.length - 1];
|
|
42
|
+
if (points.length === 2) {
|
|
43
|
+
// Quadratic bezier — 1 control point di tengah, digeser tegak lurus
|
|
44
|
+
const mx = (start.x + end.x) / 2;
|
|
45
|
+
const my = (start.y + end.y) / 2;
|
|
46
|
+
const dx = end.x - start.x;
|
|
47
|
+
const dy = end.y - start.y;
|
|
48
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
49
|
+
const bend = Math.min(dist * 0.35, 80);
|
|
50
|
+
// Control point tegak lurus dari midpoint
|
|
51
|
+
const cpx = mx - (dy / dist) * bend;
|
|
52
|
+
const cpy = my + (dx / dist) * bend;
|
|
53
|
+
return `M ${start.x} ${start.y} Q ${cpx} ${cpy}, ${end.x} ${end.y}`;
|
|
54
|
+
}
|
|
55
|
+
// Dengan waypoints — quadratic berantai lewat midpoints
|
|
56
|
+
let d = `M ${start.x} ${start.y}`;
|
|
57
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
58
|
+
const p1 = points[i];
|
|
59
|
+
const p2 = points[i + 1];
|
|
60
|
+
const cpx = (p1.x + p2.x) / 2;
|
|
61
|
+
const cpy = (p1.y + p2.y) / 2;
|
|
62
|
+
d += ` Q ${p1.x} ${p1.y}, ${cpx} ${cpy}`;
|
|
63
|
+
}
|
|
64
|
+
d += ` L ${end.x} ${end.y}`;
|
|
65
|
+
return d;
|
|
66
|
+
};
|
|
67
|
+
// ─── renderElements ──────────────────────────────────────────────────────────
|
|
68
|
+
export const renderElements = (elementEditor, mappingKey, tableMatchKey, statusKey) => {
|
|
69
|
+
return elementEditor.map((editorItem) => {
|
|
70
|
+
const isUsingMapping = mappingKey &&
|
|
71
|
+
typeof editorItem[mappingKey] === "object" &&
|
|
72
|
+
editorItem[mappingKey] !== null;
|
|
73
|
+
let finalProps = isUsingMapping ? editorItem[mappingKey] : editorItem;
|
|
74
|
+
if (tableMatchKey) {
|
|
75
|
+
const tableMatch = tableMatchKey.find((item) => item.key == (editorItem === null || editorItem === void 0 ? void 0 : editorItem[statusKey]));
|
|
76
|
+
finalProps = Object.assign(Object.assign(Object.assign({}, finalProps), tableMatch === null || tableMatch === void 0 ? void 0 : tableMatch.properties), { className: tableMatch === null || tableMatch === void 0 ? void 0 : tableMatch.className });
|
|
77
|
+
}
|
|
78
|
+
return finalProps;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
@@ -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,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
export const ConnectHandle = ({ cx, cy, width, height, nodeId, rotation = 0, isConnecting, onStartConnect, onEndConnect, }) => {
|
|
4
|
+
const hw = width / 2;
|
|
5
|
+
const hh = height / 2;
|
|
6
|
+
const [hovered, setHovered] = useState(false);
|
|
7
|
+
const isActive = isConnecting; // sedang jadi source
|
|
8
|
+
return (_jsx("g", { transform: `translate(${cx}, ${cy}) rotate(${rotation})`, children: _jsx("rect", { x: -hw - 4, y: -hh - 4, width: width + 8, height: height + 8, fill: "transparent", stroke: isActive ? "#a78bfa" // ungu kalau sedang connecting
|
|
9
|
+
: hovered ? "#38bdf8" // biru kalau hover
|
|
10
|
+
: "transparent" // invisible kalau idle
|
|
11
|
+
, strokeWidth: isActive ? 2 : 1.5, strokeDasharray: isActive ? "4 2" : "none", rx: 4, "data-connect-handle": "true", style: { cursor: "crosshair", pointerEvents: "all" }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onPointerDown: (e) => {
|
|
12
|
+
e.stopPropagation();
|
|
13
|
+
if (isActive) {
|
|
14
|
+
onEndConnect(nodeId);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
onStartConnect(nodeId);
|
|
18
|
+
}
|
|
19
|
+
} }) }));
|
|
20
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
export const ConnectHandle = ({ cx, cy, width, height, nodeId, rotation = 0, isConnecting, onStartConnect, onEndConnect, }) => {
|
|
3
|
+
const hw = width / 2;
|
|
4
|
+
const hh = height / 2;
|
|
5
|
+
const [hovered, setHovered] = useState(false);
|
|
6
|
+
const isActive = isConnecting; // sedang jadi source
|
|
7
|
+
return (<g transform={`translate(${cx}, ${cy}) rotate(${rotation})`}>
|
|
8
|
+
<rect x={-hw - 4} y={-hh - 4} width={width + 8} height={height + 8} fill="transparent" stroke={isActive ? "#a78bfa" // ungu kalau sedang connecting
|
|
9
|
+
: hovered ? "#38bdf8" // biru kalau hover
|
|
10
|
+
: "transparent" // invisible kalau idle
|
|
11
|
+
} strokeWidth={isActive ? 2 : 1.5} strokeDasharray={isActive ? "4 2" : "none"} rx={4} data-connect-handle="true" style={{ cursor: "crosshair", pointerEvents: "all" }} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onPointerDown={(e) => {
|
|
12
|
+
e.stopPropagation();
|
|
13
|
+
if (isActive) {
|
|
14
|
+
onEndConnect(nodeId);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
onStartConnect(nodeId);
|
|
18
|
+
}
|
|
19
|
+
}}/>
|
|
20
|
+
</g>);
|
|
21
|
+
};
|
|
@@ -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,191 @@
|
|
|
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) => {
|
|
20
|
+
levelMap.set(id, 0);
|
|
21
|
+
queue.push({ id, level: 0 });
|
|
22
|
+
});
|
|
23
|
+
while (queue.length > 0) {
|
|
24
|
+
const { id, level } = queue.shift();
|
|
25
|
+
edges.forEach((edge) => {
|
|
26
|
+
const neighbors = [];
|
|
27
|
+
if (edge.from === id)
|
|
28
|
+
neighbors.push(edge.to);
|
|
29
|
+
if (edge.to === id)
|
|
30
|
+
neighbors.push(edge.from);
|
|
31
|
+
neighbors.forEach((nId) => {
|
|
32
|
+
if (!levelMap.has(nId)) {
|
|
33
|
+
levelMap.set(nId, level + 1);
|
|
34
|
+
queue.push({ id: nId, level: level + 1 });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return levelMap;
|
|
40
|
+
};
|
|
41
|
+
// ─── Group edges by source ────────────────────────────────────────────────────
|
|
42
|
+
const groupBySource = (edges) => {
|
|
43
|
+
const map = new Map();
|
|
44
|
+
edges.forEach((edge) => {
|
|
45
|
+
if (!map.has(edge.from))
|
|
46
|
+
map.set(edge.from, []);
|
|
47
|
+
map.get(edge.from).push(edge);
|
|
48
|
+
});
|
|
49
|
+
return map;
|
|
50
|
+
};
|
|
51
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
52
|
+
export const ConnectionLayer = ({ edges, selectedEdge, connecting, mousePos, getNodeById, onSelectEdge,
|
|
53
|
+
// onDeleteEdge,
|
|
54
|
+
}) => {
|
|
55
|
+
const [hoveredEdge, setHoveredEdge] = useState(null);
|
|
56
|
+
// ── Flood fill highlight ──────────────────────────────────────────
|
|
57
|
+
const nodeHighlightMap = useMemo(() => {
|
|
58
|
+
if (!selectedEdge)
|
|
59
|
+
return new Map();
|
|
60
|
+
const sel = edges.find((e) => e.id === selectedEdge);
|
|
61
|
+
if (!sel)
|
|
62
|
+
return new Map();
|
|
63
|
+
return floodFill([sel.from, sel.to], edges);
|
|
64
|
+
}, [selectedEdge, edges]);
|
|
65
|
+
// ── Group by source for shared stem ──────────────────────────────
|
|
66
|
+
const sourceGroups = useMemo(() => groupBySource(edges), [edges]);
|
|
67
|
+
// ── Pre-compute curve data per edge ──────────────────────────────
|
|
68
|
+
const curveData = useMemo(() => {
|
|
69
|
+
const map = new Map();
|
|
70
|
+
edges.forEach((edge) => {
|
|
71
|
+
const fromNode = getNodeById(edge.from);
|
|
72
|
+
const toNode = getNodeById(edge.to);
|
|
73
|
+
if (!fromNode || !toNode)
|
|
74
|
+
return;
|
|
75
|
+
const start = getRectEdge(fromNode, toNode, fromNode.width, fromNode.height);
|
|
76
|
+
const end = getRectEdge(toNode, fromNode, toNode.width, toNode.height);
|
|
77
|
+
const pathD = buildCurvePath(start, end);
|
|
78
|
+
const angle = getCurveEndAngle(start, end);
|
|
79
|
+
// Midpoint di kurva (t=0.5) untuk label/badge
|
|
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 cp1;
|
|
86
|
+
let cp2;
|
|
87
|
+
if (absDx >= absDy) {
|
|
88
|
+
cp1 = { x: start.x + strength, y: start.y };
|
|
89
|
+
cp2 = { x: end.x - strength, y: end.y };
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
cp1 = { x: start.x, y: start.y + strength };
|
|
93
|
+
cp2 = { x: end.x, y: end.y - strength };
|
|
94
|
+
}
|
|
95
|
+
// Cubic bezier at t=0.5
|
|
96
|
+
const t = 0.5;
|
|
97
|
+
const mt = 1 - t;
|
|
98
|
+
const midX = mt * mt * mt * start.x +
|
|
99
|
+
3 * mt * mt * t * cp1.x +
|
|
100
|
+
3 * mt * t * t * cp2.x +
|
|
101
|
+
t * t * t * end.x;
|
|
102
|
+
const midY = mt * mt * mt * start.y +
|
|
103
|
+
3 * mt * mt * t * cp1.y +
|
|
104
|
+
3 * mt * t * t * cp2.y +
|
|
105
|
+
t * t * t * end.y;
|
|
106
|
+
map.set(edge.id, { start, end, pathD, angle, midX, midY });
|
|
107
|
+
});
|
|
108
|
+
return map;
|
|
109
|
+
}, [edges, getNodeById]);
|
|
110
|
+
// ── Shared stem exit point per source ────────────────────────────
|
|
111
|
+
const stemData = useMemo(() => {
|
|
112
|
+
const map = new Map();
|
|
113
|
+
sourceGroups.forEach((groupEdges, fromId) => {
|
|
114
|
+
if (groupEdges.length <= 1)
|
|
115
|
+
return;
|
|
116
|
+
const sourceNode = getNodeById(fromId);
|
|
117
|
+
if (!sourceNode)
|
|
118
|
+
return;
|
|
119
|
+
const exits = groupEdges
|
|
120
|
+
.map((edge) => {
|
|
121
|
+
const toNode = getNodeById(edge.to);
|
|
122
|
+
if (!toNode)
|
|
123
|
+
return null;
|
|
124
|
+
return getRectEdge(sourceNode, toNode, sourceNode.width, sourceNode.height);
|
|
125
|
+
})
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
if (exits.length === 0)
|
|
128
|
+
return;
|
|
129
|
+
map.set(fromId, {
|
|
130
|
+
x: exits.reduce((s, p) => s + p.x, 0) / exits.length,
|
|
131
|
+
y: exits.reduce((s, p) => s + p.y, 0) / exits.length,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
return map;
|
|
135
|
+
}, [sourceGroups, getNodeById]);
|
|
136
|
+
return (_jsxs("g", { id: "connection-layer", children: [Array.from(nodeHighlightMap.entries()).map(([nodeId, level]) => {
|
|
137
|
+
var _a, _b, _c;
|
|
138
|
+
const node = getNodeById(nodeId);
|
|
139
|
+
if (!node)
|
|
140
|
+
return null;
|
|
141
|
+
const w = (_a = node.width) !== null && _a !== void 0 ? _a : NODE_WIDTH;
|
|
142
|
+
const h = (_b = node.height) !== null && _b !== void 0 ? _b : NODE_HEIGHT;
|
|
143
|
+
const rotation = (_c = node.rotation) !== null && _c !== void 0 ? _c : 0;
|
|
144
|
+
const color = getLevelColor(level);
|
|
145
|
+
const padding = 4;
|
|
146
|
+
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}`));
|
|
147
|
+
}), Array.from(stemData.entries()).map(([fromId, stemExit]) => {
|
|
148
|
+
var _a;
|
|
149
|
+
const isStemOfSelected = selectedEdge &&
|
|
150
|
+
((_a = edges.find((e) => e.id === selectedEdge)) === null || _a === void 0 ? void 0 : _a.from) === fromId;
|
|
151
|
+
const color = isStemOfSelected ? "#a78bfa" : "#38bdf8";
|
|
152
|
+
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}`));
|
|
153
|
+
}), edges.map((edge) => {
|
|
154
|
+
const data = curveData.get(edge.id);
|
|
155
|
+
if (!data)
|
|
156
|
+
return null;
|
|
157
|
+
const { pathD, angle, midX, midY } = data;
|
|
158
|
+
const isSel = selectedEdge === edge.id;
|
|
159
|
+
const isHov = hoveredEdge === edge.id;
|
|
160
|
+
const markerId = `conn-arrow-${edge.id}`;
|
|
161
|
+
const markerSelId = `conn-arrow-sel-${edge.id}`;
|
|
162
|
+
// Warna edge jika dalam network highlight
|
|
163
|
+
const fromLevel = nodeHighlightMap.get(edge.from);
|
|
164
|
+
const toLevel = nodeHighlightMap.get(edge.to);
|
|
165
|
+
const isInNetwork = fromLevel !== undefined && toLevel !== undefined;
|
|
166
|
+
const networkColor = isInNetwork
|
|
167
|
+
? getLevelColor(Math.max(fromLevel, toLevel))
|
|
168
|
+
: "#38bdf8";
|
|
169
|
+
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
|
|
170
|
+
? "#a78bfa"
|
|
171
|
+
: isInNetwork && !isSel
|
|
172
|
+
? networkColor
|
|
173
|
+
: "#38bdf8", opacity: 0.9 }) }) }), _jsx("path", { d: pathD, stroke: "transparent", strokeWidth: 14, fill: "none", style: { cursor: "pointer" }, onClick: (e) => {
|
|
174
|
+
e.stopPropagation();
|
|
175
|
+
onSelectEdge(edge.id);
|
|
176
|
+
}, 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
|
|
177
|
+
? "#a78bfa"
|
|
178
|
+
: isInNetwork && selectedEdge
|
|
179
|
+
? networkColor
|
|
180
|
+
: "#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));
|
|
181
|
+
}), connecting &&
|
|
182
|
+
(() => {
|
|
183
|
+
const fromNode = getNodeById(connecting.fromId);
|
|
184
|
+
if (!fromNode)
|
|
185
|
+
return null;
|
|
186
|
+
const start = getRectEdge(fromNode, mousePos, fromNode.width, fromNode.height);
|
|
187
|
+
const pathD = buildCurvePath(start, mousePos);
|
|
188
|
+
const angle = getCurveEndAngle(start, mousePos);
|
|
189
|
+
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" } })] }));
|
|
190
|
+
})()] }));
|
|
191
|
+
};
|