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,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PropertyPanel - Node property editor panel
|
|
3
|
+
*/
|
|
4
|
+
export class PropertyPanel {
|
|
5
|
+
constructor(container, { graph, hooks, registry, render }) {
|
|
6
|
+
this.container = container;
|
|
7
|
+
this.graph = graph;
|
|
8
|
+
this.hooks = hooks;
|
|
9
|
+
this.registry = registry;
|
|
10
|
+
this.render = render; // Store render callback
|
|
11
|
+
|
|
12
|
+
this.panel = null;
|
|
13
|
+
this.currentNode = null;
|
|
14
|
+
this.isVisible = false;
|
|
15
|
+
|
|
16
|
+
this._createPanel();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_createPanel() {
|
|
20
|
+
// Create panel element
|
|
21
|
+
this.panel = document.createElement('div');
|
|
22
|
+
this.panel.className = 'property-panel';
|
|
23
|
+
this.panel.style.display = 'none';
|
|
24
|
+
|
|
25
|
+
// Panel HTML structure
|
|
26
|
+
this.panel.innerHTML = `
|
|
27
|
+
<div class="panel-inner">
|
|
28
|
+
<div class="panel-header">
|
|
29
|
+
<div class="panel-title">
|
|
30
|
+
<span class="title-text">Node Properties</span>
|
|
31
|
+
</div>
|
|
32
|
+
<button class="panel-close" type="button">×</button>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="panel-content">
|
|
35
|
+
<!-- Content will be dynamically generated -->
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
this.container.appendChild(this.panel);
|
|
41
|
+
|
|
42
|
+
// Event listeners
|
|
43
|
+
this.panel.querySelector('.panel-close').addEventListener('click', () => {
|
|
44
|
+
this.close();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Close on ESC key
|
|
48
|
+
document.addEventListener('keydown', (e) => {
|
|
49
|
+
if (e.key === 'Escape' && this.isVisible) {
|
|
50
|
+
this.close();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
open(node) {
|
|
56
|
+
if (!node) return;
|
|
57
|
+
|
|
58
|
+
this.currentNode = node;
|
|
59
|
+
this.isVisible = true;
|
|
60
|
+
|
|
61
|
+
// Update content
|
|
62
|
+
this._renderContent();
|
|
63
|
+
|
|
64
|
+
// Show panel
|
|
65
|
+
this.panel.style.display = 'block';
|
|
66
|
+
this.panel.classList.add('panel-visible');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
close() {
|
|
70
|
+
this.isVisible = false;
|
|
71
|
+
this.panel.classList.remove('panel-visible');
|
|
72
|
+
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
this.panel.style.display = 'none';
|
|
75
|
+
this.currentNode = null;
|
|
76
|
+
}, 200);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_renderContent() {
|
|
80
|
+
const node = this.currentNode;
|
|
81
|
+
if (!node) return;
|
|
82
|
+
|
|
83
|
+
const content = this.panel.querySelector('.panel-content');
|
|
84
|
+
const def = this.registry?.types?.get(node.type);
|
|
85
|
+
|
|
86
|
+
content.innerHTML = `
|
|
87
|
+
<div class="section">
|
|
88
|
+
<div class="section-title">Basic Info</div>
|
|
89
|
+
<div class="section-body">
|
|
90
|
+
<div class="field">
|
|
91
|
+
<label>Type</label>
|
|
92
|
+
<input type="text" value="${node.type}" readonly />
|
|
93
|
+
</div>
|
|
94
|
+
<div class="field">
|
|
95
|
+
<label>Title</label>
|
|
96
|
+
<input type="text" data-field="title" value="${node.title || ''}" />
|
|
97
|
+
</div>
|
|
98
|
+
<div class="field">
|
|
99
|
+
<label>ID</label>
|
|
100
|
+
<input type="text" value="${node.id}" readonly />
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="section">
|
|
106
|
+
<div class="section-title">Position & Size</div>
|
|
107
|
+
<div class="section-body">
|
|
108
|
+
<div class="field-row">
|
|
109
|
+
<div class="field">
|
|
110
|
+
<label>X</label>
|
|
111
|
+
<input type="number" data-field="x" value="${Math.round(node.computed.x)}" />
|
|
112
|
+
</div>
|
|
113
|
+
<div class="field">
|
|
114
|
+
<label>Y</label>
|
|
115
|
+
<input type="number" data-field="y" value="${Math.round(node.computed.y)}" />
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="field-row">
|
|
119
|
+
<div class="field">
|
|
120
|
+
<label>Width</label>
|
|
121
|
+
<input type="number" data-field="width" value="${node.computed.w}" />
|
|
122
|
+
</div>
|
|
123
|
+
<div class="field">
|
|
124
|
+
<label>Height</label>
|
|
125
|
+
<input type="number" data-field="height" value="${node.computed.h}" />
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
${this._renderPorts(node)}
|
|
132
|
+
${this._renderState(node)}
|
|
133
|
+
|
|
134
|
+
<div class="panel-actions">
|
|
135
|
+
<button class="btn-secondary panel-close-btn">Close</button>
|
|
136
|
+
</div>
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
// Add event listeners for inputs
|
|
140
|
+
this._attachInputListeners();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_renderPorts(node) {
|
|
144
|
+
if (!node.inputs.length && !node.outputs.length) return '';
|
|
145
|
+
|
|
146
|
+
return `
|
|
147
|
+
<div class="section">
|
|
148
|
+
<div class="section-title">Ports</div>
|
|
149
|
+
<div class="section-body">
|
|
150
|
+
${node.inputs.length ? `
|
|
151
|
+
<div class="port-group">
|
|
152
|
+
<div class="port-group-title">Inputs (${node.inputs.length})</div>
|
|
153
|
+
${node.inputs.map(p => `
|
|
154
|
+
<div class="port-item">
|
|
155
|
+
<span class="port-icon ${p.portType || 'data'}"></span>
|
|
156
|
+
<span class="port-name">${p.name}</span>
|
|
157
|
+
${p.datatype ? `<span class="port-type">${p.datatype}</span>` : ''}
|
|
158
|
+
</div>
|
|
159
|
+
`).join('')}
|
|
160
|
+
</div>
|
|
161
|
+
` : ''}
|
|
162
|
+
|
|
163
|
+
${node.outputs.length ? `
|
|
164
|
+
<div class="port-group">
|
|
165
|
+
<div class="port-group-title">Outputs (${node.outputs.length})</div>
|
|
166
|
+
${node.outputs.map(p => `
|
|
167
|
+
<div class="port-item">
|
|
168
|
+
<span class="port-icon ${p.portType || 'data'}"></span>
|
|
169
|
+
<span class="port-name">${p.name}</span>
|
|
170
|
+
${p.datatype ? `<span class="port-type">${p.datatype}</span>` : ''}
|
|
171
|
+
</div>
|
|
172
|
+
`).join('')}
|
|
173
|
+
</div>
|
|
174
|
+
` : ''}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_renderState(node) {
|
|
181
|
+
if (!node.state || Object.keys(node.state).length === 0) return '';
|
|
182
|
+
|
|
183
|
+
return `
|
|
184
|
+
<div class="section">
|
|
185
|
+
<div class="section-title">State</div>
|
|
186
|
+
<div class="section-body">
|
|
187
|
+
${Object.entries(node.state).map(([key, value]) => `
|
|
188
|
+
<div class="field">
|
|
189
|
+
<label>${key}</label>
|
|
190
|
+
<input
|
|
191
|
+
type="${typeof value === 'number' ? 'number' : 'text'}"
|
|
192
|
+
data-field="state.${key}"
|
|
193
|
+
value="${value}"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
`).join('')}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_attachInputListeners() {
|
|
203
|
+
const inputs = this.panel.querySelectorAll('[data-field]');
|
|
204
|
+
|
|
205
|
+
inputs.forEach(input => {
|
|
206
|
+
input.addEventListener('change', () => {
|
|
207
|
+
this._handleFieldChange(input.dataset.field, input.value);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Close button
|
|
212
|
+
this.panel.querySelector('.panel-close-btn').addEventListener('click', () => {
|
|
213
|
+
this.close();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_handleFieldChange(field, value) {
|
|
218
|
+
const node = this.currentNode;
|
|
219
|
+
if (!node) return;
|
|
220
|
+
|
|
221
|
+
switch (field) {
|
|
222
|
+
case 'title':
|
|
223
|
+
node.title = value;
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case 'x':
|
|
227
|
+
node.pos.x = parseFloat(value);
|
|
228
|
+
this.graph.updateWorldTransforms();
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case 'y':
|
|
232
|
+
node.pos.y = parseFloat(value);
|
|
233
|
+
this.graph.updateWorldTransforms();
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case 'width':
|
|
237
|
+
node.size.width = parseFloat(value);
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'height':
|
|
241
|
+
node.size.height = parseFloat(value);
|
|
242
|
+
break;
|
|
243
|
+
|
|
244
|
+
default:
|
|
245
|
+
// Handle state fields
|
|
246
|
+
if (field.startsWith('state.')) {
|
|
247
|
+
const key = field.substring(6);
|
|
248
|
+
if (node.state) {
|
|
249
|
+
const originalValue = node.state[key];
|
|
250
|
+
node.state[key] = typeof originalValue === 'number' ? parseFloat(value) : value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Emit update event
|
|
256
|
+
this.hooks?.emit('node:updated', node);
|
|
257
|
+
|
|
258
|
+
// Trigger render to update canvas immediately
|
|
259
|
+
if (this.render) {
|
|
260
|
+
this.render();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
destroy() {
|
|
265
|
+
if (this.panel) {
|
|
266
|
+
this.panel.remove();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function randomUUID() {
|
|
2
|
+
// 1) 전역 객체 안전 획득
|
|
3
|
+
const g =
|
|
4
|
+
typeof globalThis !== "undefined" ? globalThis :
|
|
5
|
+
typeof self !== "undefined" ? self :
|
|
6
|
+
typeof window !== "undefined" ? window :
|
|
7
|
+
typeof global !== "undefined" ? global : {};
|
|
8
|
+
|
|
9
|
+
const c = g.crypto || g.msCrypto; // IE11 호환
|
|
10
|
+
|
|
11
|
+
// 2) 네이티브 지원 (브라우저/Deno 등)
|
|
12
|
+
if (c && typeof c.randomUUID === "function") {
|
|
13
|
+
return c.randomUUID();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 3) Web Crypto만 있는 경우 (getRandomValues로 직접 생성)
|
|
17
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
18
|
+
const bytes = new Uint8Array(16);
|
|
19
|
+
c.getRandomValues(bytes);
|
|
20
|
+
// RFC4122 버전/변형 비트 설정
|
|
21
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
22
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
23
|
+
|
|
24
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
|
25
|
+
return (
|
|
26
|
+
hex.slice(0, 4).join("") + "-" +
|
|
27
|
+
hex.slice(4, 6).join("") + "-" +
|
|
28
|
+
hex.slice(6, 8).join("") + "-" +
|
|
29
|
+
hex.slice(8, 10).join("") + "-" +
|
|
30
|
+
hex.slice(10, 16).join("")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 4) Node.js 전용 대체 (require가 있을 때)
|
|
35
|
+
try {
|
|
36
|
+
// 번들러/ESM 충돌 피하려고 런타임에만 require 접근
|
|
37
|
+
|
|
38
|
+
const req = Function('return typeof require === "function" ? require : null')();
|
|
39
|
+
if (req) {
|
|
40
|
+
const nodeCrypto = req("crypto");
|
|
41
|
+
if (typeof nodeCrypto.randomUUID === "function") {
|
|
42
|
+
return nodeCrypto.randomUUID();
|
|
43
|
+
}
|
|
44
|
+
const bytes = nodeCrypto.randomBytes(16);
|
|
45
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
46
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
47
|
+
|
|
48
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
|
49
|
+
return (
|
|
50
|
+
hex.slice(0, 4).join("") + "-" +
|
|
51
|
+
hex.slice(4, 6).join("") + "-" +
|
|
52
|
+
hex.slice(6, 8).join("") + "-" +
|
|
53
|
+
hex.slice(8, 10).join("") + "-" +
|
|
54
|
+
hex.slice(10, 16).join("")
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 5) 최후의 비보안 대체 (CSPRNG 아님!)
|
|
62
|
+
const bytes = new Uint8Array(16);
|
|
63
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
64
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
65
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
66
|
+
|
|
67
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
|
68
|
+
return (
|
|
69
|
+
hex.slice(0, 4).join("") + "-" +
|
|
70
|
+
hex.slice(4, 6).join("") + "-" +
|
|
71
|
+
hex.slice(6, 8).join("") + "-" +
|
|
72
|
+
hex.slice(8, 10).join("") + "-" +
|
|
73
|
+
hex.slice(10, 16).join("")
|
|
74
|
+
);
|
|
75
|
+
}
|