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,161 @@
1
+ // 캔버스 위에 붙는 DOM 오버레이. 캔버스의 scale/offset에 맞춰 CSS transform을 적용.
2
+ // 동기화 하기 까다로워서 아직 적용하지 않음
3
+ export class HtmlOverlay {
4
+ /**
5
+ * @param {HTMLElement} host 캔버스를 감싸는 래퍼( position: relative )
6
+ * @param {CanvasRenderer} renderer
7
+ * @param {Registry} registry
8
+ */
9
+ constructor(host, renderer, registry) {
10
+ this.host = host;
11
+ this.renderer = renderer;
12
+ this.registry = registry;
13
+ this.container = document.createElement("div");
14
+ Object.assign(this.container.style, {
15
+ position: "absolute",
16
+ inset: "0",
17
+ pointerEvents: "none", // 기본은 통과
18
+ zIndex: "10",
19
+ });
20
+ host.appendChild(this.container);
21
+
22
+ /** @type {Map<string, HTMLElement>} */
23
+ this.nodes = new Map();
24
+ }
25
+
26
+ /** 기본 노드 레이아웃 생성 (헤더 + 바디) */
27
+ _createDefaultNodeLayout(node) {
28
+ const container = document.createElement("div");
29
+ container.className = "node-overlay";
30
+ Object.assign(container.style, {
31
+ position: "absolute",
32
+ display: "flex",
33
+ flexDirection: "column",
34
+ boxSizing: "border-box",
35
+ pointerEvents: "none", // 기본은 통과 (캔버스 인터랙션 위해)
36
+ overflow: "hidden", // 둥근 모서리 등
37
+ });
38
+
39
+ const header = document.createElement("div");
40
+ header.className = "node-header";
41
+ Object.assign(header.style, {
42
+ height: "24px",
43
+ flexShrink: "0",
44
+ display: "flex",
45
+ alignItems: "center",
46
+ padding: "0 8px",
47
+ cursor: "grab",
48
+ userSelect: "none",
49
+ pointerEvents: "none", // 헤더 클릭시 드래그는 캔버스가 처리
50
+ });
51
+
52
+ const body = document.createElement("div");
53
+ body.className = "node-body";
54
+ Object.assign(body.style, {
55
+ flex: "1",
56
+ position: "relative",
57
+ overflow: "hidden",
58
+ pointerEvents: "auto", // 바디 내부는 인터랙션 가능하게? 아니면 이것도 none하고 자식만 auto?
59
+ // 일단 바디는 auto로 두면 바디 영역 클릭시 드래그가 안됨.
60
+ // 그래서 바디도 none으로 하고, 내부 컨텐츠(input 등)만 auto로 하는게 맞음.
61
+ pointerEvents: "none",
62
+ });
63
+
64
+ container.appendChild(header);
65
+ container.appendChild(body);
66
+
67
+ // 나중에 접근하기 쉽게 프로퍼티로 저장
68
+ container._domParts = { header, body };
69
+ return container;
70
+ }
71
+
72
+ /** 노드용 엘리먼트 생성(한 번만) */
73
+ _ensureNodeElement(node, def) {
74
+ let el = this.nodes.get(node.id);
75
+ if (!el) {
76
+ // 1) 사용자 정의 render 함수가 있으면 우선 사용
77
+ if (def.html?.render) {
78
+ el = def.html.render(node);
79
+ }
80
+ // 2) 아니면 기본 레이아웃 사용 (html 설정이 있는 경우)
81
+ else if (def.html) {
82
+ el = this._createDefaultNodeLayout(node);
83
+ // 초기화 훅
84
+ if (def.html.init) {
85
+ def.html.init(node, el, el._domParts);
86
+ }
87
+ } else {
88
+ return null; // HTML 없음
89
+ }
90
+
91
+ if (!el) return null;
92
+
93
+ el.style.position = "absolute";
94
+ el.style.pointerEvents = "none"; // 기본적으로 캔버스 통과
95
+ this.container.appendChild(el);
96
+ this.nodes.set(node.id, el);
97
+ }
98
+ return el;
99
+ }
100
+
101
+ /** 그래프와 변환 동기화하여 렌더링 */
102
+ draw(graph, selection = new Set()) {
103
+ // 컨테이너 전체에 월드 변환 적용 (CSS 픽셀 기준)
104
+ const { scale, offsetX, offsetY } = this.renderer;
105
+ this.container.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
106
+ this.container.style.transformOrigin = "0 0";
107
+
108
+ const seen = new Set();
109
+
110
+ for (const node of graph.nodes.values()) {
111
+ const def = this.registry.types.get(node.type);
112
+
113
+ // render 함수가 있거나, html 설정 객체가 있으면 처리
114
+ const hasHtml = !!(def?.html);
115
+ if (!hasHtml) continue;
116
+
117
+ const el = this._ensureNodeElement(node, def);
118
+ if (!el) continue;
119
+
120
+ // 노드 위치/크기 동기화 (월드 좌표 → 컨테이너 내부는 이미 scale/translate 적용)
121
+ el.style.left = `${node.computed.x}px`;
122
+ el.style.top = `${node.computed.y}px`;
123
+ el.style.width = `${node.computed.w}px`;
124
+ el.style.height = `${node.computed.h}px`;
125
+
126
+ // 선택 상태 등 업데이트 훅
127
+ if (def.html.update) {
128
+ // 기본 레이아웃이면 header/body도 함께 전달
129
+ const parts = el._domParts || {};
130
+ def.html.update(node, el, {
131
+ selected: selection.has(node.id),
132
+ header: parts.header,
133
+ body: parts.body
134
+ });
135
+ }
136
+
137
+ seen.add(node.id);
138
+ }
139
+
140
+ // 없어진 노드 제거
141
+ for (const [id, el] of this.nodes) {
142
+ if (!seen.has(id)) {
143
+ el.remove();
144
+ this.nodes.delete(id);
145
+ }
146
+ }
147
+ }
148
+
149
+ clear() {
150
+ // Remove all node elements
151
+ for (const [, el] of this.nodes) {
152
+ el.remove();
153
+ }
154
+ this.nodes.clear();
155
+ }
156
+
157
+ destroy() {
158
+ this.clear();
159
+ this.container.remove();
160
+ }
161
+ }
@@ -0,0 +1,38 @@
1
+ // src/render/hitTest.js
2
+ export function hitTestNode(node, x, y) {
3
+ const { x: nx, y: ny, w: width, h: height } = node.computed || {
4
+ x: node.pos.x,
5
+ y: node.pos.y,
6
+ w: node.size.width,
7
+ h: node.size.height,
8
+ };
9
+ return x >= nx && x <= nx + width && y >= ny && y <= ny + height;
10
+ }
11
+
12
+ export function portRect(node, port, idx, dir) {
13
+ const { x: nx, y: ny, w: width, h: height } = node.computed || {
14
+ x: node.pos.x,
15
+ y: node.pos.y,
16
+ w: node.size.width,
17
+ h: node.size.height,
18
+ };
19
+
20
+ // Calculate port count for better spacing
21
+ const portCount = dir === "in" ? node.inputs.length : node.outputs.length;
22
+ const headerHeight = 28;
23
+ const availableHeight = (height || node.size.height) - headerHeight - 16;
24
+ const spacing = availableHeight / (portCount + 1);
25
+
26
+ const y = ny + headerHeight + spacing * (idx + 1);
27
+
28
+ // Ports centered on node edges (half inside, half outside)
29
+ const portWidth = 12;
30
+ const portHeight = 12;
31
+
32
+ if (dir === "in") {
33
+ return { x: nx - portWidth / 2, y: y - portHeight / 2, w: portWidth, h: portHeight };
34
+ }
35
+ if (dir === "out") {
36
+ return { x: nx + width - portWidth / 2, y: y - portHeight / 2, w: portWidth, h: portHeight };
37
+ }
38
+ }
@@ -0,0 +1,277 @@
1
+ /* Property Panel Styles */
2
+ .property-panel {
3
+ position: absolute;
4
+ top: 0;
5
+ right: 0;
6
+ height: 100%;
7
+ width: 380px;
8
+ z-index: 9999;
9
+
10
+ background: radial-gradient(circle at top right, #3c3c3c 0, #1f1f1f 55%);
11
+ background-color: #262626;
12
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
13
+ border-left: 1px solid rgba(255, 255, 255, 0.06);
14
+
15
+ opacity: 0;
16
+ transform: translateX(20px);
17
+ transition: all 0.25s ease-out;
18
+ pointer-events: none;
19
+ }
20
+
21
+ .property-panel.panel-visible {
22
+ opacity: 1;
23
+ transform: translateX(0);
24
+ pointer-events: all;
25
+ }
26
+
27
+ .panel-inner {
28
+ height: 100%;
29
+ display: flex;
30
+ flex-direction: column;
31
+ padding: 20px;
32
+ overflow: hidden;
33
+ }
34
+
35
+ .panel-header {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ padding-bottom: 16px;
40
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
41
+ margin-bottom: 20px;
42
+ flex-shrink: 0;
43
+ }
44
+
45
+ .panel-title {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 10px;
49
+ }
50
+
51
+ .title-text {
52
+ font-size: 18px;
53
+ font-weight: 600;
54
+ color: #f5f5f5;
55
+ letter-spacing: 0.01em;
56
+ }
57
+
58
+ .panel-close {
59
+ width: 32px;
60
+ height: 32px;
61
+ border: none;
62
+ border-radius: 50%;
63
+ background: transparent;
64
+ color: #bdbdbd;
65
+ font-size: 24px;
66
+ cursor: pointer;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ transition: all 0.2s ease;
71
+ }
72
+
73
+ .panel-close:hover {
74
+ background: rgba(255, 255, 255, 0.08);
75
+ color: #fff;
76
+ transform: translateY(-1px);
77
+ }
78
+
79
+ .panel-content {
80
+ flex: 1;
81
+ overflow-y: auto;
82
+ overflow-x: hidden;
83
+ padding-right: 6px;
84
+ min-height: 0;
85
+ }
86
+
87
+ /* Scrollbar */
88
+ .panel-content::-webkit-scrollbar {
89
+ width: 6px;
90
+ }
91
+
92
+ .panel-content::-webkit-scrollbar-track {
93
+ background: transparent;
94
+ }
95
+
96
+ .panel-content::-webkit-scrollbar-thumb {
97
+ background: rgba(255, 255, 255, 0.1);
98
+ border-radius: 3px;
99
+ }
100
+
101
+ .panel-content::-webkit-scrollbar-thumb:hover {
102
+ background: rgba(255, 255, 255, 0.15);
103
+ }
104
+
105
+ /* Section */
106
+ .section {
107
+ margin-bottom: 20px;
108
+ padding: 14px;
109
+ border-radius: 10px;
110
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
111
+ border: 1px solid rgba(255, 255, 255, 0.05);
112
+ }
113
+
114
+ .section-title {
115
+ font-size: 13px;
116
+ font-weight: 600;
117
+ color: #d0d0d0;
118
+ margin-bottom: 12px;
119
+ text-transform: uppercase;
120
+ letter-spacing: 0.08em;
121
+ }
122
+
123
+ .section-body {
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 10px;
127
+ }
128
+
129
+ /* Fields */
130
+ .field {
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: 4px;
134
+ }
135
+
136
+ .field label {
137
+ font-size: 12px;
138
+ color: #9e9e9e;
139
+ font-weight: 500;
140
+ }
141
+
142
+ .field input {
143
+ width: 100%;
144
+ padding: 8px 10px;
145
+ border-radius: 6px;
146
+ border: 1px solid rgba(255, 255, 255, 0.12);
147
+ background: rgba(0, 0, 0, 0.25);
148
+ color: #f5f5f5;
149
+ font-size: 13px;
150
+ outline: none;
151
+ transition: all 0.2s ease;
152
+ font-family:
153
+ system-ui,
154
+ -apple-system,
155
+ sans-serif;
156
+ box-sizing: border-box;
157
+ line-height: 1.4;
158
+ }
159
+
160
+ .field input:focus {
161
+ border-color: #6366f1;
162
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
163
+ background: rgba(0, 0, 0, 0.35);
164
+ }
165
+
166
+ .field input[readonly] {
167
+ background: rgba(0, 0, 0, 0.4);
168
+ color: #888;
169
+ cursor: not-allowed;
170
+ opacity: 0.7;
171
+ }
172
+
173
+ .field-row {
174
+ display: grid;
175
+ grid-template-columns: 1fr 1fr;
176
+ gap: 10px;
177
+ }
178
+
179
+ /* Port Items */
180
+ .port-group {
181
+ margin-top: 8px;
182
+ }
183
+
184
+ .port-group-title {
185
+ font-size: 11px;
186
+ font-weight: 600;
187
+ color: #a0a0a0;
188
+ margin-bottom: 6px;
189
+ text-transform: uppercase;
190
+ letter-spacing: 0.05em;
191
+ }
192
+
193
+ .port-item {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 8px;
197
+ padding: 6px 8px;
198
+ border-radius: 4px;
199
+ background: rgba(0, 0, 0, 0.2);
200
+ margin-bottom: 4px;
201
+ }
202
+
203
+ .port-icon {
204
+ width: 8px;
205
+ height: 8px;
206
+ border-radius: 50%;
207
+ flex-shrink: 0;
208
+ }
209
+
210
+ .port-icon.exec {
211
+ background: #10b981;
212
+ border-radius: 2px;
213
+ }
214
+
215
+ .port-icon.data {
216
+ background: #6366f1;
217
+ }
218
+
219
+ .port-name {
220
+ flex: 1;
221
+ font-size: 12px;
222
+ color: #e0e0e0;
223
+ font-weight: 500;
224
+ }
225
+
226
+ .port-type {
227
+ font-size: 10px;
228
+ color: #888;
229
+ padding: 2px 6px;
230
+ background: rgba(255, 255, 255, 0.05);
231
+ border-radius: 3px;
232
+ }
233
+
234
+ /* Actions */
235
+ .panel-actions {
236
+ display: flex;
237
+ gap: 10px;
238
+ padding-top: 16px;
239
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
240
+ margin-top: 16px;
241
+ }
242
+
243
+ .panel-actions button {
244
+ flex: 1;
245
+ padding: 10px 16px;
246
+ border: none;
247
+ border-radius: 6px;
248
+ font-size: 13px;
249
+ font-weight: 600;
250
+ cursor: pointer;
251
+ transition: all 0.2s ease;
252
+ font-family:
253
+ system-ui,
254
+ -apple-system,
255
+ sans-serif;
256
+ }
257
+
258
+ .btn-primary {
259
+ background: #6366f1;
260
+ color: #fff;
261
+ }
262
+
263
+ .btn-primary:hover {
264
+ background: #818cf8;
265
+ transform: translateY(-1px);
266
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
267
+ }
268
+
269
+ .btn-secondary {
270
+ background: rgba(255, 255, 255, 0.08);
271
+ color: #d0d0d0;
272
+ }
273
+
274
+ .btn-secondary:hover {
275
+ background: rgba(255, 255, 255, 0.12);
276
+ color: #fff;
277
+ }