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.
Files changed (145) hide show
  1. package/dist/app/constant.d.ts +1 -1
  2. package/dist/app/graph-view/page.d.ts +1 -0
  3. package/dist/app/graph-view/page.js +343 -0
  4. package/dist/app/graph-view/page.jsx +445 -0
  5. package/dist/app/graph-view-new/constant.d.ts +581 -0
  6. package/dist/app/graph-view-new/constant.js +6973 -0
  7. package/dist/app/graph-view-new/page.d.ts +1 -0
  8. package/dist/app/graph-view-new/page.js +71 -0
  9. package/dist/app/graph-view-new/page.jsx +98 -0
  10. package/dist/app/layout.d.ts +1 -1
  11. package/dist/app/new-board/page.d.ts +1 -1
  12. package/dist/app/new-board/page.js +43 -7
  13. package/dist/app/new-board/page.jsx +45 -12
  14. package/dist/app/old-board/page.d.ts +1 -2
  15. package/dist/app/only-view/chair.d.ts +1 -1
  16. package/dist/app/only-view/chair.js +2 -10
  17. package/dist/app/only-view/page.d.ts +1 -1
  18. package/dist/app/only-view/user.d.ts +1 -1
  19. package/dist/app/only-view/user.js +2 -10
  20. package/dist/app/page.d.ts +1 -1
  21. package/dist/app/test/page.d.ts +1 -2
  22. package/dist/app/v2/page.d.ts +1 -1
  23. package/dist/components/button-tools/index.d.ts +1 -1
  24. package/dist/components/button-tools/index.js +7 -5
  25. package/dist/components/button-tools/index.jsx +21 -9
  26. package/dist/components/form-tools/label.d.ts +1 -1
  27. package/dist/components/form-tools/label.js +9 -20
  28. package/dist/components/form-tools/label.jsx +38 -28
  29. package/dist/components/form-tools/shape.d.ts +1 -1
  30. package/dist/components/form-tools/shape.js +5 -5
  31. package/dist/components/form-tools/shape.jsx +8 -8
  32. package/dist/components/input/number-indicator.d.ts +1 -1
  33. package/dist/components/joystick/index.d.ts +1 -2
  34. package/dist/components/layer/index.d.ts +1 -1
  35. package/dist/components/layer-v2/index.d.ts +1 -1
  36. package/dist/components/layer-v3/index.d.ts +1 -1
  37. package/dist/components/layer-v3/index.js +44 -3
  38. package/dist/components/layer-v3/index.jsx +120 -3
  39. package/dist/components/layer-v4/index.d.ts +1 -1
  40. package/dist/components/layer-v5/constant.d.ts +60 -0
  41. package/dist/components/layer-v5/constant.js +93 -0
  42. package/dist/components/layer-v5/index.d.ts +24 -0
  43. package/dist/components/layer-v5/index.js +927 -0
  44. package/dist/components/layer-v5/index.jsx +1049 -0
  45. package/dist/components/lib/index.d.ts +1 -1
  46. package/dist/components/modal-preview/index.d.ts +1 -1
  47. package/dist/features/board/index.d.ts +1 -1
  48. package/dist/features/board-v2/index.d.ts +1 -2
  49. package/dist/features/board-v3/index.d.ts +1 -1
  50. package/dist/features/board-v3/index.js +350 -72
  51. package/dist/features/board-v3/index.jsx +369 -75
  52. package/dist/features/board-v3/resize-element.js +5 -0
  53. package/dist/features/board-v3/utils.d.ts +8 -0
  54. package/dist/features/board-v3/utils.js +23 -7
  55. package/dist/features/navbar/index.d.ts +1 -1
  56. package/dist/features/package/index.d.ts +3 -1
  57. package/dist/features/package/index.js +1 -1
  58. package/dist/features/package/index.jsx +6 -1
  59. package/dist/features/panel/index.d.ts +9 -1
  60. package/dist/features/panel/index.js +160 -38
  61. package/dist/features/panel/index.jsx +173 -46
  62. package/dist/features/panel/polygon.d.ts +2 -0
  63. package/dist/features/panel/polygon.js +44 -0
  64. package/dist/features/panel/polygon.jsx +70 -0
  65. package/dist/features/panel/select-tool.d.ts +1 -1
  66. package/dist/features/panel/select-tool.js +3 -0
  67. package/dist/features/panel/select-tool.jsx +3 -0
  68. package/dist/features/panel/selected-group.d.ts +1 -1
  69. package/dist/features/panel/selected-group.js +24 -26
  70. package/dist/features/panel/selected-group.jsx +56 -51
  71. package/dist/features/panel/square-circle-tool.d.ts +1 -1
  72. package/dist/features/panel/table-seat-circle.d.ts +1 -1
  73. package/dist/features/panel/table-seat-square.d.ts +1 -1
  74. package/dist/features/panel/text-tool.d.ts +1 -1
  75. package/dist/features/panel/text-tool.js +17 -2
  76. package/dist/features/panel/text-tool.jsx +19 -2
  77. package/dist/features/panel/upload-tool.d.ts +1 -1
  78. package/dist/features/panel/upload-tool.js +17 -3
  79. package/dist/features/panel/upload-tool.jsx +23 -4
  80. package/dist/features/side-tool/index.d.ts +1 -1
  81. package/dist/features/side-tool/index.js +43 -6
  82. package/dist/features/side-tool/index.jsx +47 -10
  83. package/dist/features/view-only/index.d.ts +1 -1
  84. package/dist/features/view-only-2/index.d.ts +1 -1
  85. package/dist/features/view-only-3/index.d.ts +1 -1
  86. package/dist/features/view-only-4/connect-handle.d.ts +13 -0
  87. package/dist/features/view-only-4/connect-handle.js +23 -0
  88. package/dist/features/view-only-4/connect-handle.jsx +30 -0
  89. package/dist/features/view-only-4/connection-layer.d.ts +21 -0
  90. package/dist/features/view-only-4/connection-layer.js +219 -0
  91. package/dist/features/view-only-4/connection-layer.jsx +291 -0
  92. package/dist/features/view-only-4/index.d.ts +99 -0
  93. package/dist/features/view-only-4/index.js +684 -0
  94. package/dist/features/view-only-4/index.jsx +722 -0
  95. package/dist/features/view-only-4/integration-guide.d.ts +0 -0
  96. package/dist/features/view-only-4/integration-guide.js +0 -0
  97. package/dist/features/view-only-4/use-connection-graph.d.ts +41 -0
  98. package/dist/features/view-only-4/use-connection-graph.js +182 -0
  99. package/dist/features/view-only-4/utils.d.ts +74 -0
  100. package/dist/features/view-only-4/utils.js +106 -0
  101. package/dist/features/view-only-5/connect-handle.d.ts +30 -0
  102. package/dist/features/view-only-5/connect-handle.js +88 -0
  103. package/dist/features/view-only-5/connect-handle.jsx +96 -0
  104. package/dist/features/view-only-5/connection-layer.d.ts +34 -0
  105. package/dist/features/view-only-5/connection-layer.js +182 -0
  106. package/dist/features/view-only-5/connection-layer.jsx +265 -0
  107. package/dist/features/view-only-5/index.d.ts +102 -0
  108. package/dist/features/view-only-5/index.js +585 -0
  109. package/dist/features/view-only-5/index.jsx +614 -0
  110. package/dist/features/view-only-5/use-connection-graph.d.ts +57 -0
  111. package/dist/features/view-only-5/use-connection-graph.js +196 -0
  112. package/dist/features/view-only-5/utils.d.ts +52 -0
  113. package/dist/features/view-only-5/utils.js +80 -0
  114. package/dist/features/view-only-6/connect-handle.d.ts +13 -0
  115. package/dist/features/view-only-6/connect-handle.js +20 -0
  116. package/dist/features/view-only-6/connect-handle.jsx +21 -0
  117. package/dist/features/view-only-6/connection-layer.d.ts +22 -0
  118. package/dist/features/view-only-6/connection-layer.js +191 -0
  119. package/dist/features/view-only-6/connection-layer.jsx +244 -0
  120. package/dist/features/view-only-6/index.d.ts +99 -0
  121. package/dist/features/view-only-6/index.js +687 -0
  122. package/dist/features/view-only-6/index.jsx +724 -0
  123. package/dist/features/view-only-6/use-connection-graph.d.ts +26 -0
  124. package/dist/features/view-only-6/use-connection-graph.js +103 -0
  125. package/dist/features/view-only-6/utils.d.ts +66 -0
  126. package/dist/features/view-only-6/utils.js +96 -0
  127. package/dist/features/view-only-7/connect-handle.d.ts +13 -0
  128. package/dist/features/view-only-7/connect-handle.js +23 -0
  129. package/dist/features/view-only-7/connect-handle.jsx +30 -0
  130. package/dist/features/view-only-7/connection-layer.d.ts +22 -0
  131. package/dist/features/view-only-7/connection-layer.js +165 -0
  132. package/dist/features/view-only-7/connection-layer.jsx +217 -0
  133. package/dist/features/view-only-7/index.d.ts +99 -0
  134. package/dist/features/view-only-7/index.js +687 -0
  135. package/dist/features/view-only-7/index.jsx +724 -0
  136. package/dist/features/view-only-7/use-connection-graph.d.ts +26 -0
  137. package/dist/features/view-only-7/use-connection-graph.js +104 -0
  138. package/dist/features/view-only-7/utils.d.ts +69 -0
  139. package/dist/features/view-only-7/utils.js +144 -0
  140. package/dist/index.d.ts +2 -1
  141. package/dist/index.js +2 -1
  142. package/dist/provider/redux-provider.d.ts +1 -1
  143. package/dist/provider/store-provider.d.ts +1 -1
  144. package/dist/seat-editor.css +1 -1
  145. package/package.json +1 -1
@@ -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,21 @@
1
+ import React from "react";
2
+ import { NodeType } from "./utils";
3
+ import { EdgeType } from "./utils";
4
+ type Props = {
5
+ edges: EdgeType[];
6
+ selectedEdge: string | null;
7
+ connecting: {
8
+ fromId: string;
9
+ } | null;
10
+ mousePos: {
11
+ x: number;
12
+ y: number;
13
+ };
14
+ getNodeById: (id: string) => NodeType | null;
15
+ onSelectEdge: (edgeId: string) => void;
16
+ onStartDragWaypoint: (e: React.MouseEvent, edgeId: string, index: number) => void;
17
+ onInsertWaypoint: (e: React.MouseEvent, edgeId: string, insertIndex: number, x: number, y: number) => void;
18
+ onRemoveWaypoint: (e: React.MouseEvent, edgeId: string, index: number) => void;
19
+ };
20
+ export declare const ConnectionLayer: React.FC<Props>;
21
+ export {};
@@ -0,0 +1,219 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // components/ConnectionLayer.tsx
3
+ import { useMemo } from "react";
4
+ import { buildPath, getRectEdge, snap, GRID_SIZE, NODE_WIDTH, NODE_HEIGHT } from "./utils";
5
+ const LEVEL_COLORS = [
6
+ "#22c55e",
7
+ "#f59e0b",
8
+ "#f97316",
9
+ "#ef4444",
10
+ "#a78bfa",
11
+ "#38bdf8",
12
+ ];
13
+ const getLevelColor = (level) => LEVEL_COLORS[Math.min(level, LEVEL_COLORS.length - 1)];
14
+ // ── BFS flood fill ────────────────────────────────────────────────────
15
+ const floodFill = (seedIds, edges) => {
16
+ const levelMap = new Map();
17
+ const queue = [];
18
+ seedIds.forEach((id) => { levelMap.set(id, 0); queue.push({ id, level: 0 }); });
19
+ while (queue.length > 0) {
20
+ const { id, level } = queue.shift();
21
+ edges.forEach((edge) => {
22
+ const neighbors = [];
23
+ if (edge.from === id)
24
+ neighbors.push(edge.to);
25
+ if (edge.to === id)
26
+ neighbors.push(edge.from);
27
+ neighbors.forEach((nId) => {
28
+ if (!levelMap.has(nId)) {
29
+ levelMap.set(nId, level + 1);
30
+ queue.push({ id: nId, level: level + 1 });
31
+ }
32
+ });
33
+ });
34
+ }
35
+ return levelMap;
36
+ };
37
+ // ── Arrow angle dari segmen TERAKHIR path ────────────────────────────
38
+ /**
39
+ * Lihat 2 titik terakhir dari allPoints untuk tahu arah segmen akhir.
40
+ * buildPath routing H midX V curr.y H curr.x:
41
+ * - Segmen terakhir selalu H → kecuali jika lurus vertikal
42
+ * Tapi kita cek arah prev→last secara dominan untuk akurasi.
43
+ */
44
+ const getArrowAngle = (prev, last) => {
45
+ const dx = last.x - prev.x;
46
+ const dy = last.y - prev.y;
47
+ const absDx = Math.abs(dx);
48
+ const absDy = Math.abs(dy);
49
+ const threshold = 2; // presisi tinggi
50
+ // Lurus vertikal
51
+ if (absDx <= threshold)
52
+ return dy > 0 ? 90 : 270;
53
+ // Lurus horizontal
54
+ if (absDy <= threshold)
55
+ return dx > 0 ? 0 : 180;
56
+ // Untuk orthogonal H midX V curr.y H curr.x:
57
+ // segmen terakhir adalah H → arah kiri/kanan
58
+ return dx > 0 ? 0 : 180;
59
+ };
60
+ // ── Hitung split point untuk group of edges dari sumber yang sama ────
61
+ //
62
+ // Strategi:
63
+ // 1. Hitung exit point masing-masing edge dari source
64
+ // 2. Rata-rata semua exit point → "stem exit" (titik keluar bersama)
65
+ // 3. Stem: source center → stem exit
66
+ // 4. Branch: stem exit → waypoints → target
67
+ //
68
+ const getStemAndBranches = (sourceNode, targetNodes) => {
69
+ // Exit point tiap edge jika berjalan sendiri
70
+ const exits = targetNodes.map(({ node }) => getRectEdge(sourceNode, node, sourceNode.width, sourceNode.height));
71
+ // Rata-rata exit → shared stem exit point
72
+ const avgX = snap(exits.reduce((s, p) => s + p.x, 0) / exits.length);
73
+ const avgY = snap(exits.reduce((s, p) => s + p.y, 0) / exits.length);
74
+ const stemExit = { x: avgX, y: avgY };
75
+ // Untuk setiap branch: stem exit → waypoints edge → target edge point
76
+ const branches = targetNodes.map(({ node, edge }) => {
77
+ const end = getRectEdge(node, sourceNode, node.width, node.height);
78
+ const branchPoints = [stemExit, ...edge.waypoints, end];
79
+ return { edge, branchPoints, end };
80
+ });
81
+ return { stemExit, branches };
82
+ };
83
+ export const ConnectionLayer = ({ edges, selectedEdge, connecting, mousePos, getNodeById, onSelectEdge, onStartDragWaypoint, onInsertWaypoint, onRemoveWaypoint, }) => {
84
+ // ── Flood fill highlight ──────────────────────────────────────────
85
+ const nodeHighlightMap = useMemo(() => {
86
+ if (!selectedEdge)
87
+ return new Map();
88
+ const sel = edges.find((e) => e.id === selectedEdge);
89
+ if (!sel)
90
+ return new Map();
91
+ return floodFill([sel.from, sel.to], edges);
92
+ }, [selectedEdge, edges]);
93
+ // ── Group edges by source (from) ──────────────────────────────────
94
+ // sourceGroups: Map<fromId, EdgeType[]>
95
+ const sourceGroups = useMemo(() => {
96
+ const map = new Map();
97
+ edges.forEach((edge) => {
98
+ if (!map.has(edge.from))
99
+ map.set(edge.from, []);
100
+ map.get(edge.from).push(edge);
101
+ });
102
+ return map;
103
+ }, [edges]);
104
+ // ── Pre-compute stem & branch paths ──────────────────────────────
105
+ // stemPaths: Map<fromId, { stemPath, stemExit }>
106
+ // branchPaths: Map<edgeId, { branchPoints, angle }>
107
+ const { stemPaths, branchPaths } = useMemo(() => {
108
+ const stemPaths = new Map();
109
+ const branchPaths = new Map();
110
+ sourceGroups.forEach((groupEdges, fromId) => {
111
+ const sourceNode = getNodeById(fromId);
112
+ if (!sourceNode)
113
+ return;
114
+ const targetNodes = groupEdges
115
+ .map((edge) => {
116
+ const node = getNodeById(edge.to);
117
+ return node ? { node, edge } : null;
118
+ })
119
+ .filter(Boolean);
120
+ if (targetNodes.length === 0)
121
+ return;
122
+ if (targetNodes.length === 1) {
123
+ // Hanya 1 target — tidak perlu branching, render normal
124
+ const { node, edge } = targetNodes[0];
125
+ const start = getRectEdge(sourceNode, node, sourceNode.width, sourceNode.height);
126
+ const end = getRectEdge(node, sourceNode, node.width, node.height);
127
+ const allPoints = [start, ...edge.waypoints, end];
128
+ const pathD = buildPath(allPoints);
129
+ const angle = getArrowAngle(allPoints[allPoints.length - 2], allPoints[allPoints.length - 1]);
130
+ branchPaths.set(edge.id, { pathD, angle, allPoints });
131
+ return;
132
+ }
133
+ // Multiple targets → branching
134
+ const { stemExit, branches } = getStemAndBranches(sourceNode, targetNodes);
135
+ // Stem path: source exit → stem exit
136
+ // Gunakan exit point dari source ke arah rata-rata (stemExit)
137
+ const sourceExit = getRectEdge(sourceNode, stemExit, sourceNode.width, sourceNode.height);
138
+ const stemPoints = [sourceExit, stemExit];
139
+ stemPaths.set(fromId, {
140
+ pathD: buildPath(stemPoints),
141
+ stemExit,
142
+ });
143
+ // Branch paths
144
+ branches.forEach(({ edge, branchPoints }) => {
145
+ const angle = getArrowAngle(branchPoints[branchPoints.length - 2], branchPoints[branchPoints.length - 1]);
146
+ branchPaths.set(edge.id, {
147
+ pathD: buildPath(branchPoints),
148
+ angle,
149
+ allPoints: branchPoints,
150
+ });
151
+ });
152
+ });
153
+ return { stemPaths, branchPaths };
154
+ }, [sourceGroups, getNodeById, edges]);
155
+ return (_jsxs("g", { id: "connection-layer", children: [Array.from(nodeHighlightMap.entries()).map(([nodeId, level]) => {
156
+ var _a, _b, _c;
157
+ const node = getNodeById(nodeId);
158
+ if (!node)
159
+ return null;
160
+ const w = (_a = node.width) !== null && _a !== void 0 ? _a : NODE_WIDTH;
161
+ const h = (_b = node.height) !== null && _b !== void 0 ? _b : NODE_HEIGHT;
162
+ const rotation = (_c = node.rotation) !== null && _c !== void 0 ? _c : 0;
163
+ const color = getLevelColor(level);
164
+ const padding = 4;
165
+ 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}`));
166
+ }), edges.map((edge) => {
167
+ const fromLevel = nodeHighlightMap.get(edge.from);
168
+ const toLevel = nodeHighlightMap.get(edge.to);
169
+ if (fromLevel === undefined || toLevel === undefined)
170
+ return null;
171
+ if (edge.id === selectedEdge)
172
+ return null;
173
+ const bp = branchPaths.get(edge.id);
174
+ if (!bp)
175
+ return null;
176
+ const color = getLevelColor(Math.max(fromLevel, toLevel));
177
+ return (_jsx("path", { d: bp.pathD, stroke: color, strokeWidth: 1.5, fill: "none", opacity: 0.35, strokeDasharray: "4 3", style: { pointerEvents: "none" } }, `network-edge-${edge.id}`));
178
+ }), Array.from(stemPaths.entries()).map(([fromId, { pathD }]) => {
179
+ var _a;
180
+ const isStemOfSelected = selectedEdge && ((_a = edges.find((e) => e.id === selectedEdge)) === null || _a === void 0 ? void 0 : _a.from) === fromId;
181
+ return (_jsxs("g", { children: [_jsx("path", { d: pathD, stroke: "#38bdf8", strokeWidth: 6, fill: "none", opacity: 0.08, style: { pointerEvents: "none" } }), _jsx("path", { d: pathD, stroke: isStemOfSelected ? "#a78bfa" : "#38bdf8", strokeWidth: isStemOfSelected ? 2.5 : 2, fill: "none", opacity: 0.9, style: { pointerEvents: "none" } })] }, `stem-${fromId}`));
182
+ }), Array.from(stemPaths.entries()).map(([fromId, { stemExit }]) => {
183
+ var _a;
184
+ const isStemOfSelected = selectedEdge && ((_a = edges.find((e) => e.id === selectedEdge)) === null || _a === void 0 ? void 0 : _a.from) === fromId;
185
+ const color = isStemOfSelected ? "#a78bfa" : "#38bdf8";
186
+ return (_jsxs("g", { style: { pointerEvents: "none" }, children: [_jsx("circle", { cx: stemExit.x, cy: stemExit.y, r: 6, fill: "#0f172a", stroke: color, strokeWidth: 2 }), _jsx("circle", { cx: stemExit.x, cy: stemExit.y, r: 3, fill: color })] }, `split-dot-${fromId}`));
187
+ }), edges.map((edge) => {
188
+ const fromNode = getNodeById(edge.from);
189
+ const toNode = getNodeById(edge.to);
190
+ if (!fromNode || !toNode)
191
+ return null;
192
+ const bp = branchPaths.get(edge.id);
193
+ if (!bp)
194
+ return null;
195
+ const { pathD, angle, allPoints } = bp;
196
+ const isSel = selectedEdge === edge.id;
197
+ const markerId = `conn-arrow-${edge.id}`;
198
+ const markerSelId = `conn-arrow-sel-${edge.id}`;
199
+ return (_jsxs("g", { children: [_jsxs("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: "#38bdf8", opacity: 0.9 }) }), _jsx("marker", { id: markerSelId, 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" }) })] }), _jsx("path", { d: pathD, stroke: "transparent", strokeWidth: 14, fill: "none", style: { cursor: "pointer" }, onClick: (e) => { e.stopPropagation(); onSelectEdge(edge.id); } }), isSel && (_jsx("path", { d: pathD, stroke: "#a78bfa", strokeWidth: 7, fill: "none", opacity: 0.12, style: { pointerEvents: "none" } })), _jsx("path", { d: pathD, stroke: isSel ? "#a78bfa" : "#38bdf8", strokeWidth: isSel ? 2 : 1.5, fill: "none", opacity: 0.9, markerEnd: `url(#${isSel ? markerSelId : markerId})`, style: { pointerEvents: "none" } }), edge.waypoints.map((wp, i) => (_jsxs("g", { children: [_jsx("circle", { cx: wp.x, cy: wp.y, r: 12, fill: "transparent", style: { cursor: "grab" }, onMouseDown: (e) => onStartDragWaypoint(e, edge.id, i), onContextMenu: (e) => onRemoveWaypoint(e, edge.id, i) }), _jsx("circle", { cx: wp.x, cy: wp.y, r: isSel ? 8 : 5, fill: isSel ? "#1e1b2e" : "#0f172a", stroke: isSel ? "#a78bfa" : "#38bdf8", strokeWidth: isSel ? 2 : 1.2, style: { pointerEvents: "none" } }), _jsx("circle", { cx: wp.x, cy: wp.y, r: isSel ? 3 : 2, fill: isSel ? "#a78bfa" : "#38bdf8", style: { pointerEvents: "none" } })] }, `wp-${edge.id}-${i}`))), isSel && allPoints.slice(0, -1).map((pt, i) => {
200
+ const next = allPoints[i + 1];
201
+ const mx = snap((pt.x + next.x) / 2);
202
+ const my = snap((pt.y + next.y) / 2);
203
+ const tooClose = edge.waypoints.some((wp) => Math.abs(wp.x - mx) < GRID_SIZE && Math.abs(wp.y - my) < GRID_SIZE);
204
+ if (tooClose)
205
+ return null;
206
+ return (_jsxs("g", { children: [_jsx("circle", { cx: mx, cy: my, r: 10, fill: "transparent", style: { cursor: "crosshair" }, onMouseDown: (e) => onInsertWaypoint(e, edge.id, i, mx, my) }), _jsx("circle", { cx: mx, cy: my, r: 4, fill: "#1e1b2e", stroke: "#a78bfa", strokeWidth: 1, strokeDasharray: "2 2", style: { pointerEvents: "none" } }), _jsx("circle", { cx: mx, cy: my, r: 1.5, fill: "#a78bfa", style: { pointerEvents: "none" } })] }, `mid-${edge.id}-${i}`));
207
+ })] }, edge.id));
208
+ }), connecting && (() => {
209
+ const fromNode = getNodeById(connecting.fromId);
210
+ if (!fromNode)
211
+ return null;
212
+ // Gunakan mousePos exact (tidak di-snap) agar preview ikut mouse persis
213
+ const start = getRectEdge(fromNode, mousePos, fromNode.width, fromNode.height);
214
+ const allPoints = [start, { x: mousePos.x, y: mousePos.y }];
215
+ const pathD = buildPath(allPoints);
216
+ const angle = getArrowAngle(allPoints[allPoints.length - 2], allPoints[allPoints.length - 1]);
217
+ return (_jsxs("g", { children: [_jsx("defs", { children: _jsx("marker", { id: "conn-arrow-preview-live", 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-arrow-preview-live)", opacity: 0.8, style: { pointerEvents: "none" } })] }));
218
+ })()] }));
219
+ };
@@ -0,0 +1,291 @@
1
+ // components/ConnectionLayer.tsx
2
+ import React, { useMemo } from "react";
3
+ import { buildPath, getRectEdge, snap, GRID_SIZE, NODE_WIDTH, NODE_HEIGHT } from "./utils";
4
+ const LEVEL_COLORS = [
5
+ "#22c55e",
6
+ "#f59e0b",
7
+ "#f97316",
8
+ "#ef4444",
9
+ "#a78bfa",
10
+ "#38bdf8",
11
+ ];
12
+ const getLevelColor = (level) => LEVEL_COLORS[Math.min(level, LEVEL_COLORS.length - 1)];
13
+ // ── BFS flood fill ────────────────────────────────────────────────────
14
+ const floodFill = (seedIds, edges) => {
15
+ const levelMap = new Map();
16
+ const queue = [];
17
+ seedIds.forEach((id) => { levelMap.set(id, 0); queue.push({ id, level: 0 }); });
18
+ while (queue.length > 0) {
19
+ const { id, level } = queue.shift();
20
+ edges.forEach((edge) => {
21
+ const neighbors = [];
22
+ if (edge.from === id)
23
+ neighbors.push(edge.to);
24
+ if (edge.to === id)
25
+ neighbors.push(edge.from);
26
+ neighbors.forEach((nId) => {
27
+ if (!levelMap.has(nId)) {
28
+ levelMap.set(nId, level + 1);
29
+ queue.push({ id: nId, level: level + 1 });
30
+ }
31
+ });
32
+ });
33
+ }
34
+ return levelMap;
35
+ };
36
+ // ── Arrow angle dari segmen TERAKHIR path ────────────────────────────
37
+ /**
38
+ * Lihat 2 titik terakhir dari allPoints untuk tahu arah segmen akhir.
39
+ * buildPath routing H midX V curr.y H curr.x:
40
+ * - Segmen terakhir selalu H → kecuali jika lurus vertikal
41
+ * Tapi kita cek arah prev→last secara dominan untuk akurasi.
42
+ */
43
+ const getArrowAngle = (prev, last) => {
44
+ const dx = last.x - prev.x;
45
+ const dy = last.y - prev.y;
46
+ const absDx = Math.abs(dx);
47
+ const absDy = Math.abs(dy);
48
+ const threshold = 2; // presisi tinggi
49
+ // Lurus vertikal
50
+ if (absDx <= threshold)
51
+ return dy > 0 ? 90 : 270;
52
+ // Lurus horizontal
53
+ if (absDy <= threshold)
54
+ return dx > 0 ? 0 : 180;
55
+ // Untuk orthogonal H midX V curr.y H curr.x:
56
+ // segmen terakhir adalah H → arah kiri/kanan
57
+ return dx > 0 ? 0 : 180;
58
+ };
59
+ // ── Hitung split point untuk group of edges dari sumber yang sama ────
60
+ //
61
+ // Strategi:
62
+ // 1. Hitung exit point masing-masing edge dari source
63
+ // 2. Rata-rata semua exit point → "stem exit" (titik keluar bersama)
64
+ // 3. Stem: source center → stem exit
65
+ // 4. Branch: stem exit → waypoints → target
66
+ //
67
+ const getStemAndBranches = (sourceNode, targetNodes) => {
68
+ // Exit point tiap edge jika berjalan sendiri
69
+ const exits = targetNodes.map(({ node }) => getRectEdge(sourceNode, node, sourceNode.width, sourceNode.height));
70
+ // Rata-rata exit → shared stem exit point
71
+ const avgX = snap(exits.reduce((s, p) => s + p.x, 0) / exits.length);
72
+ const avgY = snap(exits.reduce((s, p) => s + p.y, 0) / exits.length);
73
+ const stemExit = { x: avgX, y: avgY };
74
+ // Untuk setiap branch: stem exit → waypoints edge → target edge point
75
+ const branches = targetNodes.map(({ node, edge }) => {
76
+ const end = getRectEdge(node, sourceNode, node.width, node.height);
77
+ const branchPoints = [stemExit, ...edge.waypoints, end];
78
+ return { edge, branchPoints, end };
79
+ });
80
+ return { stemExit, branches };
81
+ };
82
+ export const ConnectionLayer = ({ edges, selectedEdge, connecting, mousePos, getNodeById, onSelectEdge, onStartDragWaypoint, onInsertWaypoint, onRemoveWaypoint, }) => {
83
+ // ── Flood fill highlight ──────────────────────────────────────────
84
+ const nodeHighlightMap = useMemo(() => {
85
+ if (!selectedEdge)
86
+ return new Map();
87
+ const sel = edges.find((e) => e.id === selectedEdge);
88
+ if (!sel)
89
+ return new Map();
90
+ return floodFill([sel.from, sel.to], edges);
91
+ }, [selectedEdge, edges]);
92
+ // ── Group edges by source (from) ──────────────────────────────────
93
+ // sourceGroups: Map<fromId, EdgeType[]>
94
+ const sourceGroups = useMemo(() => {
95
+ const map = new Map();
96
+ edges.forEach((edge) => {
97
+ if (!map.has(edge.from))
98
+ map.set(edge.from, []);
99
+ map.get(edge.from).push(edge);
100
+ });
101
+ return map;
102
+ }, [edges]);
103
+ // ── Pre-compute stem & branch paths ──────────────────────────────
104
+ // stemPaths: Map<fromId, { stemPath, stemExit }>
105
+ // branchPaths: Map<edgeId, { branchPoints, angle }>
106
+ const { stemPaths, branchPaths } = useMemo(() => {
107
+ const stemPaths = new Map();
108
+ const branchPaths = new Map();
109
+ sourceGroups.forEach((groupEdges, fromId) => {
110
+ const sourceNode = getNodeById(fromId);
111
+ if (!sourceNode)
112
+ return;
113
+ const targetNodes = groupEdges
114
+ .map((edge) => {
115
+ const node = getNodeById(edge.to);
116
+ return node ? { node, edge } : null;
117
+ })
118
+ .filter(Boolean);
119
+ if (targetNodes.length === 0)
120
+ return;
121
+ if (targetNodes.length === 1) {
122
+ // Hanya 1 target — tidak perlu branching, render normal
123
+ const { node, edge } = targetNodes[0];
124
+ const start = getRectEdge(sourceNode, node, sourceNode.width, sourceNode.height);
125
+ const end = getRectEdge(node, sourceNode, node.width, node.height);
126
+ const allPoints = [start, ...edge.waypoints, end];
127
+ const pathD = buildPath(allPoints);
128
+ const angle = getArrowAngle(allPoints[allPoints.length - 2], allPoints[allPoints.length - 1]);
129
+ branchPaths.set(edge.id, { pathD, angle, allPoints });
130
+ return;
131
+ }
132
+ // Multiple targets → branching
133
+ const { stemExit, branches } = getStemAndBranches(sourceNode, targetNodes);
134
+ // Stem path: source exit → stem exit
135
+ // Gunakan exit point dari source ke arah rata-rata (stemExit)
136
+ const sourceExit = getRectEdge(sourceNode, stemExit, sourceNode.width, sourceNode.height);
137
+ const stemPoints = [sourceExit, stemExit];
138
+ stemPaths.set(fromId, {
139
+ pathD: buildPath(stemPoints),
140
+ stemExit,
141
+ });
142
+ // Branch paths
143
+ branches.forEach(({ edge, branchPoints }) => {
144
+ const angle = getArrowAngle(branchPoints[branchPoints.length - 2], branchPoints[branchPoints.length - 1]);
145
+ branchPaths.set(edge.id, {
146
+ pathD: buildPath(branchPoints),
147
+ angle,
148
+ allPoints: branchPoints,
149
+ });
150
+ });
151
+ });
152
+ return { stemPaths, branchPaths };
153
+ }, [sourceGroups, getNodeById, edges]);
154
+ return (<g id="connection-layer">
155
+
156
+ {/* ── Node border highlight (flood fill) ───────────────────────── */}
157
+ {Array.from(nodeHighlightMap.entries()).map(([nodeId, level]) => {
158
+ var _a, _b, _c;
159
+ const node = getNodeById(nodeId);
160
+ if (!node)
161
+ return null;
162
+ const w = (_a = node.width) !== null && _a !== void 0 ? _a : NODE_WIDTH;
163
+ const h = (_b = node.height) !== null && _b !== void 0 ? _b : NODE_HEIGHT;
164
+ const rotation = (_c = node.rotation) !== null && _c !== void 0 ? _c : 0;
165
+ const color = getLevelColor(level);
166
+ const padding = 4;
167
+ return (<g key={`highlight-${nodeId}`} transform={`translate(${node.x}, ${node.y}) rotate(${rotation})`} style={{ pointerEvents: "none" }}>
168
+ <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}/>
169
+ <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}/>
170
+ <g transform={`translate(${w / 2 + padding - 2}, ${-h / 2 - padding})`}>
171
+ <circle r={7} fill={color} opacity={0.9}/>
172
+ <text textAnchor="middle" dominantBaseline="middle" fill="white" fontSize={7} fontFamily="monospace" fontWeight={700} style={{ userSelect: "none" }}>{level}</text>
173
+ </g>
174
+ </g>);
175
+ })}
176
+
177
+ {/* ── Edge network highlight (edges dalam jaringan) ────────────── */}
178
+ {edges.map((edge) => {
179
+ const fromLevel = nodeHighlightMap.get(edge.from);
180
+ const toLevel = nodeHighlightMap.get(edge.to);
181
+ if (fromLevel === undefined || toLevel === undefined)
182
+ return null;
183
+ if (edge.id === selectedEdge)
184
+ return null;
185
+ const bp = branchPaths.get(edge.id);
186
+ if (!bp)
187
+ return null;
188
+ const color = getLevelColor(Math.max(fromLevel, toLevel));
189
+ return (<path key={`network-edge-${edge.id}`} d={bp.pathD} stroke={color} strokeWidth={1.5} fill="none" opacity={0.35} strokeDasharray="4 3" style={{ pointerEvents: "none" }}/>);
190
+ })}
191
+
192
+ {/* ── Stem lines (shared source exit) ──────────────────────────── */}
193
+ {Array.from(stemPaths.entries()).map(([fromId, { pathD }]) => {
194
+ var _a;
195
+ const isStemOfSelected = selectedEdge && ((_a = edges.find((e) => e.id === selectedEdge)) === null || _a === void 0 ? void 0 : _a.from) === fromId;
196
+ return (<g key={`stem-${fromId}`}>
197
+ {/* Glow */}
198
+ <path d={pathD} stroke="#38bdf8" strokeWidth={6} fill="none" opacity={0.08} style={{ pointerEvents: "none" }}/>
199
+ {/* Stem line */}
200
+ <path d={pathD} stroke={isStemOfSelected ? "#a78bfa" : "#38bdf8"} strokeWidth={isStemOfSelected ? 2.5 : 2} fill="none" opacity={0.9} style={{ pointerEvents: "none" }}/>
201
+ </g>);
202
+ })}
203
+
204
+ {/* ── Split dot di titik cabang ─────────────────────────────────── */}
205
+ {Array.from(stemPaths.entries()).map(([fromId, { stemExit }]) => {
206
+ var _a;
207
+ const isStemOfSelected = selectedEdge && ((_a = edges.find((e) => e.id === selectedEdge)) === null || _a === void 0 ? void 0 : _a.from) === fromId;
208
+ const color = isStemOfSelected ? "#a78bfa" : "#38bdf8";
209
+ return (<g key={`split-dot-${fromId}`} style={{ pointerEvents: "none" }}>
210
+ <circle cx={stemExit.x} cy={stemExit.y} r={6} fill="#0f172a" stroke={color} strokeWidth={2}/>
211
+ <circle cx={stemExit.x} cy={stemExit.y} r={3} fill={color}/>
212
+ </g>);
213
+ })}
214
+
215
+ {/* ── Branch lines + waypoints ──────────────────────────────────── */}
216
+ {edges.map((edge) => {
217
+ const fromNode = getNodeById(edge.from);
218
+ const toNode = getNodeById(edge.to);
219
+ if (!fromNode || !toNode)
220
+ return null;
221
+ const bp = branchPaths.get(edge.id);
222
+ if (!bp)
223
+ return null;
224
+ const { pathD, angle, allPoints } = bp;
225
+ const isSel = selectedEdge === edge.id;
226
+ const markerId = `conn-arrow-${edge.id}`;
227
+ const markerSelId = `conn-arrow-sel-${edge.id}`;
228
+ return (<g key={edge.id}>
229
+ <defs>
230
+ <marker id={markerId} markerWidth="8" markerHeight="8" refX="7" refY="3" orient={`${angle}deg`}>
231
+ <path d="M 0 0 L 8 3 L 0 6 Z" fill="#38bdf8" opacity={0.9}/>
232
+ </marker>
233
+ <marker id={markerSelId} markerWidth="8" markerHeight="8" refX="7" refY="3" orient={`${angle}deg`}>
234
+ <path d="M 0 0 L 8 3 L 0 6 Z" fill="#a78bfa"/>
235
+ </marker>
236
+ </defs>
237
+
238
+ {/* Hit area */}
239
+ <path d={pathD} stroke="transparent" strokeWidth={14} fill="none" style={{ cursor: "pointer" }} onClick={(e) => { e.stopPropagation(); onSelectEdge(edge.id); }}/>
240
+
241
+ {/* Glow selected */}
242
+ {isSel && (<path d={pathD} stroke="#a78bfa" strokeWidth={7} fill="none" opacity={0.12} style={{ pointerEvents: "none" }}/>)}
243
+
244
+ {/* Branch line */}
245
+ <path d={pathD} stroke={isSel ? "#a78bfa" : "#38bdf8"} strokeWidth={isSel ? 2 : 1.5} fill="none" opacity={0.9} markerEnd={`url(#${isSel ? markerSelId : markerId})`} style={{ pointerEvents: "none" }}/>
246
+
247
+ {/* Waypoints */}
248
+ {edge.waypoints.map((wp, i) => (<g key={`wp-${edge.id}-${i}`}>
249
+ <circle cx={wp.x} cy={wp.y} r={12} fill="transparent" style={{ cursor: "grab" }} onMouseDown={(e) => onStartDragWaypoint(e, edge.id, i)} onContextMenu={(e) => onRemoveWaypoint(e, edge.id, i)}/>
250
+ <circle cx={wp.x} cy={wp.y} r={isSel ? 8 : 5} fill={isSel ? "#1e1b2e" : "#0f172a"} stroke={isSel ? "#a78bfa" : "#38bdf8"} strokeWidth={isSel ? 2 : 1.2} style={{ pointerEvents: "none" }}/>
251
+ <circle cx={wp.x} cy={wp.y} r={isSel ? 3 : 2} fill={isSel ? "#a78bfa" : "#38bdf8"} style={{ pointerEvents: "none" }}/>
252
+ </g>))}
253
+
254
+ {/* Midpoint insert handles */}
255
+ {isSel && allPoints.slice(0, -1).map((pt, i) => {
256
+ const next = allPoints[i + 1];
257
+ const mx = snap((pt.x + next.x) / 2);
258
+ const my = snap((pt.y + next.y) / 2);
259
+ const tooClose = edge.waypoints.some((wp) => Math.abs(wp.x - mx) < GRID_SIZE && Math.abs(wp.y - my) < GRID_SIZE);
260
+ if (tooClose)
261
+ return null;
262
+ return (<g key={`mid-${edge.id}-${i}`}>
263
+ <circle cx={mx} cy={my} r={10} fill="transparent" style={{ cursor: "crosshair" }} onMouseDown={(e) => onInsertWaypoint(e, edge.id, i, mx, my)}/>
264
+ <circle cx={mx} cy={my} r={4} fill="#1e1b2e" stroke="#a78bfa" strokeWidth={1} strokeDasharray="2 2" style={{ pointerEvents: "none" }}/>
265
+ <circle cx={mx} cy={my} r={1.5} fill="#a78bfa" style={{ pointerEvents: "none" }}/>
266
+ </g>);
267
+ })}
268
+ </g>);
269
+ })}
270
+
271
+ {/* ── Preview line saat connecting ─────────────────────────────── */}
272
+ {connecting && (() => {
273
+ const fromNode = getNodeById(connecting.fromId);
274
+ if (!fromNode)
275
+ return null;
276
+ // Gunakan mousePos exact (tidak di-snap) agar preview ikut mouse persis
277
+ const start = getRectEdge(fromNode, mousePos, fromNode.width, fromNode.height);
278
+ const allPoints = [start, { x: mousePos.x, y: mousePos.y }];
279
+ const pathD = buildPath(allPoints);
280
+ const angle = getArrowAngle(allPoints[allPoints.length - 2], allPoints[allPoints.length - 1]);
281
+ return (<g>
282
+ <defs>
283
+ <marker id="conn-arrow-preview-live" markerWidth="8" markerHeight="8" refX="7" refY="3" orient={`${angle}deg`}>
284
+ <path d="M 0 0 L 8 3 L 0 6 Z" fill="#a78bfa" opacity={0.85}/>
285
+ </marker>
286
+ </defs>
287
+ <path d={pathD} stroke="#a78bfa" strokeWidth={1.5} fill="none" strokeDasharray="6 3" markerEnd="url(#conn-arrow-preview-live)" opacity={0.8} style={{ pointerEvents: "none" }}/>
288
+ </g>);
289
+ })()}
290
+ </g>);
291
+ };