html-overlay-node 0.1.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/LICENSE +21 -0
- package/dist/example.json +522 -0
- package/dist/html-overlay-node.es.js +3596 -0
- package/dist/html-overlay-node.es.js.map +1 -0
- package/dist/html-overlay-node.umd.js +2 -0
- package/dist/html-overlay-node.umd.js.map +1 -0
- package/index.css +232 -0
- package/package.json +65 -0
- package/readme.md +437 -0
- package/src/core/CommandStack.js +26 -0
- package/src/core/Edge.js +28 -0
- package/src/core/Edge.test.js +73 -0
- package/src/core/Graph.js +267 -0
- package/src/core/Graph.test.js +256 -0
- package/src/core/Group.js +77 -0
- package/src/core/Hooks.js +12 -0
- package/src/core/Hooks.test.js +108 -0
- package/src/core/Node.js +70 -0
- package/src/core/Node.test.js +113 -0
- package/src/core/Registry.js +71 -0
- package/src/core/Registry.test.js +88 -0
- package/src/core/Runner.js +211 -0
- package/src/core/commands.js +125 -0
- package/src/groups/GroupManager.js +116 -0
- package/src/index.js +1030 -0
- package/src/interact/ContextMenu.js +400 -0
- package/src/interact/Controller.js +856 -0
- package/src/minimap/Minimap.js +146 -0
- package/src/render/CanvasRenderer.js +606 -0
- package/src/render/HtmlOverlay.js +161 -0
- package/src/render/hitTest.js +38 -0
- package/src/ui/PropertyPanel.css +277 -0
- package/src/ui/PropertyPanel.js +269 -0
- package/src/utils/utils.js +75 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Find an edge id by its endpoints (fallback for undo)
|
|
2
|
+
function findEdgeId(graph, a, b, c, d) {
|
|
3
|
+
for (const [id, e] of graph.edges) {
|
|
4
|
+
if (
|
|
5
|
+
e.fromNode === a &&
|
|
6
|
+
e.fromPort === b &&
|
|
7
|
+
e.toNode === c &&
|
|
8
|
+
e.toPort === d
|
|
9
|
+
)
|
|
10
|
+
return id;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function MoveNodeCmd(node, fromPos, toPos) {
|
|
16
|
+
return {
|
|
17
|
+
do() {
|
|
18
|
+
node.pos = { ...toPos };
|
|
19
|
+
},
|
|
20
|
+
undo() {
|
|
21
|
+
node.pos = { ...fromPos };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AddEdgeCmd(graph, fromNode, fromPort, toNode, toPort) {
|
|
27
|
+
let addedId = null;
|
|
28
|
+
return {
|
|
29
|
+
do() {
|
|
30
|
+
graph.addEdge(fromNode, fromPort, toNode, toPort);
|
|
31
|
+
addedId = findEdgeId(graph, fromNode, fromPort, toNode, toPort);
|
|
32
|
+
},
|
|
33
|
+
undo() {
|
|
34
|
+
const id =
|
|
35
|
+
addedId ?? findEdgeId(graph, fromNode, fromPort, toNode, toPort);
|
|
36
|
+
if (id != null) graph.edges.delete(id);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function RemoveEdgeCmd(graph, edgeId) {
|
|
42
|
+
const e = graph.edges.get(edgeId);
|
|
43
|
+
if (!e) return null;
|
|
44
|
+
// capture for undo
|
|
45
|
+
const { fromNode, fromPort, toNode, toPort } = e;
|
|
46
|
+
return {
|
|
47
|
+
do() {
|
|
48
|
+
graph.edges.delete(edgeId);
|
|
49
|
+
},
|
|
50
|
+
undo() {
|
|
51
|
+
graph.addEdge(fromNode, fromPort, toNode, toPort);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Optional: group multiple commands as one (used for "rewire")
|
|
57
|
+
export function CompoundCmd(cmds) {
|
|
58
|
+
return {
|
|
59
|
+
do() {
|
|
60
|
+
cmds.forEach((c) => c?.do());
|
|
61
|
+
},
|
|
62
|
+
undo() {
|
|
63
|
+
[...cmds].reverse().forEach((c) => c?.undo());
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function RemoveNodeCmd(graph, node) {
|
|
69
|
+
let removedNode = null;
|
|
70
|
+
let removedEdges = [];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
do() {
|
|
74
|
+
// Store the node and its connected edges for undo
|
|
75
|
+
removedNode = node;
|
|
76
|
+
removedEdges = graph.edges
|
|
77
|
+
? [...graph.edges.values()].filter((e) => {
|
|
78
|
+
return e.fromNode === node.id || e.toNode === node.id;
|
|
79
|
+
})
|
|
80
|
+
: [];
|
|
81
|
+
|
|
82
|
+
// Remove edges first
|
|
83
|
+
for (const edge of removedEdges) {
|
|
84
|
+
graph.edges.delete(edge.id);
|
|
85
|
+
}
|
|
86
|
+
// Remove the node
|
|
87
|
+
graph.nodes.delete(node.id);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
undo() {
|
|
91
|
+
// Restore node
|
|
92
|
+
if (removedNode) {
|
|
93
|
+
graph.nodes.set(removedNode.id, removedNode);
|
|
94
|
+
}
|
|
95
|
+
// Restore edges
|
|
96
|
+
for (const edge of removedEdges) {
|
|
97
|
+
graph.edges.set(edge.id, edge);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function ResizeNodeCmd(node, fromSize, toSize) {
|
|
104
|
+
return {
|
|
105
|
+
do() {
|
|
106
|
+
node.size.width = toSize.w;
|
|
107
|
+
node.size.height = toSize.h;
|
|
108
|
+
},
|
|
109
|
+
undo() {
|
|
110
|
+
node.size.width = fromSize.w;
|
|
111
|
+
node.size.height = fromSize.h;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function ChangeGroupColorCmd(node, fromColor, toColor) {
|
|
117
|
+
return {
|
|
118
|
+
do() {
|
|
119
|
+
node.state.color = toColor;
|
|
120
|
+
},
|
|
121
|
+
undo() {
|
|
122
|
+
node.state.color = fromColor;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/groups/GroupManager.js
|
|
2
|
+
import { randomUUID } from "/src/utils/utils.js";
|
|
3
|
+
|
|
4
|
+
export class GroupManager {
|
|
5
|
+
constructor({ graph, hooks }) {
|
|
6
|
+
this.graph = graph;
|
|
7
|
+
this.hooks = hooks;
|
|
8
|
+
this._groups = [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ---------- CRUD ----------
|
|
12
|
+
addGroup({
|
|
13
|
+
title = "Group",
|
|
14
|
+
x = 0,
|
|
15
|
+
y = 0,
|
|
16
|
+
width = 240,
|
|
17
|
+
height = 160,
|
|
18
|
+
color = "#39424e",
|
|
19
|
+
members = [],
|
|
20
|
+
} = {}) {
|
|
21
|
+
// Validate parameters
|
|
22
|
+
if (width < 100 || height < 60) {
|
|
23
|
+
console.warn("Group size too small, using minimum size");
|
|
24
|
+
width = Math.max(100, width);
|
|
25
|
+
height = Math.max(60, height);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const groupNode = this.graph.addNode("core/Group", {
|
|
29
|
+
title,
|
|
30
|
+
x,
|
|
31
|
+
y,
|
|
32
|
+
width,
|
|
33
|
+
height,
|
|
34
|
+
});
|
|
35
|
+
groupNode.state.color = color;
|
|
36
|
+
|
|
37
|
+
// Reparent members with validation
|
|
38
|
+
for (const memberId of members) {
|
|
39
|
+
const node = this.graph.getNodeById(memberId);
|
|
40
|
+
if (node) {
|
|
41
|
+
if (node.type === "core/Group") {
|
|
42
|
+
console.warn(`Cannot add group ${memberId} as member of another group`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
this.graph.reparent(node, groupNode);
|
|
46
|
+
} else {
|
|
47
|
+
console.warn(`Member node ${memberId} not found, skipping`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this._groups.push(groupNode);
|
|
52
|
+
this.hooks?.emit("group:change");
|
|
53
|
+
return groupNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
addGroupFromSelection({ title = "Group", margin = { x: 12, y: 12 } } = {}) {
|
|
57
|
+
// Controller에서 selection을 받아와야 함
|
|
58
|
+
// 여기서는 간단히 graph.nodes를 순회하며 selected 상태를 확인한다고 가정하거나
|
|
59
|
+
// 외부에서 members를 넘겨받는 것이 좋음
|
|
60
|
+
// 일단은 외부에서 members를 넘겨받는 addGroup을 활용.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
removeGroup(id) {
|
|
65
|
+
const groupNode = this.graph.getNodeById(id);
|
|
66
|
+
if (!groupNode || groupNode.type !== "core/Group") return;
|
|
67
|
+
|
|
68
|
+
// Ungroup: reparent children to group's parent
|
|
69
|
+
const children = [...groupNode.children];
|
|
70
|
+
for (const child of children) {
|
|
71
|
+
this.graph.reparent(child, groupNode.parent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.graph.removeNode(id);
|
|
75
|
+
this.hooks?.emit("group:change");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------- 이동/리사이즈 ----------
|
|
79
|
+
// 이제 Node의 이동/리사이즈 로직을 따름.
|
|
80
|
+
// Controller에서 Node 이동 시 updateWorldTransforms가 호출되므로 자동 처리됨.
|
|
81
|
+
|
|
82
|
+
resizeGroup(id, dw, dh) {
|
|
83
|
+
const g = this.graph.getNodeById(id);
|
|
84
|
+
if (!g || g.type !== "core/Group") return;
|
|
85
|
+
|
|
86
|
+
const minW = 100;
|
|
87
|
+
const minH = 60;
|
|
88
|
+
g.size.width = Math.max(minW, g.size.width + dw);
|
|
89
|
+
g.size.height = Math.max(minH, g.size.height + dh);
|
|
90
|
+
|
|
91
|
+
this.graph.updateWorldTransforms();
|
|
92
|
+
this.hooks?.emit("group:change");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------- 히트테스트 & 드래그 ----------
|
|
96
|
+
// 이제 Group도 Node이므로 Controller의 Node 히트테스트 로직을 따름.
|
|
97
|
+
// 단, Resize Handle은 별도 처리가 필요할 수 있음.
|
|
98
|
+
|
|
99
|
+
hitTestResizeHandle(x, y) {
|
|
100
|
+
const handleSize = 10;
|
|
101
|
+
// 역순 순회 (위에 있는 것부터)
|
|
102
|
+
const nodes = [...this.graph.nodes.values()].reverse();
|
|
103
|
+
|
|
104
|
+
for (const node of nodes) {
|
|
105
|
+
if (node.type !== "core/Group") continue;
|
|
106
|
+
|
|
107
|
+
// World Transform 사용
|
|
108
|
+
const { x: gx, y: gy, w: gw, h: gh } = node.computed;
|
|
109
|
+
|
|
110
|
+
if (x >= gx + gw - handleSize && x <= gx + gw && y >= gy + gh - handleSize && y <= gy + gh) {
|
|
111
|
+
return { group: node, handle: "se" };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|