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.
@@ -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
+ }