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,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
|
+
};
|