pynode-flow 0.2.0__py3-none-any.whl

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.
Files changed (227) hide show
  1. pynode/__init__.py +37 -0
  2. pynode/__main__.py +9 -0
  3. pynode/_perf.py +65 -0
  4. pynode/_version.py +24 -0
  5. pynode/api/__init__.py +15 -0
  6. pynode/api/helpers.py +79 -0
  7. pynode/api/nodes.py +423 -0
  8. pynode/api/services.py +124 -0
  9. pynode/api/sse.py +58 -0
  10. pynode/api/uploads.py +59 -0
  11. pynode/api/workflows.py +466 -0
  12. pynode/config.py +93 -0
  13. pynode/install_nodes.py +143 -0
  14. pynode/main.py +118 -0
  15. pynode/node_registry.py +156 -0
  16. pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
  17. pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
  18. pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
  19. pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
  20. pynode/nodes/BatchNode/__init__.py +3 -0
  21. pynode/nodes/BatchNode/batch_node.py +86 -0
  22. pynode/nodes/BatchNode/requirements.txt +1 -0
  23. pynode/nodes/CameraNode/__init__.py +4 -0
  24. pynode/nodes/CameraNode/camera_node.py +204 -0
  25. pynode/nodes/CameraNode/requirements.txt +2 -0
  26. pynode/nodes/ChangeNode/__init__.py +1 -0
  27. pynode/nodes/ChangeNode/change_node.py +255 -0
  28. pynode/nodes/ChangeNode/requirements.txt +1 -0
  29. pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
  30. pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
  31. pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
  32. pynode/nodes/CounterNode/__init__.py +3 -0
  33. pynode/nodes/CounterNode/counter_node.py +191 -0
  34. pynode/nodes/CropNode/__init__.py +4 -0
  35. pynode/nodes/CropNode/crop_node.py +317 -0
  36. pynode/nodes/CropNode/requirements.txt +2 -0
  37. pynode/nodes/DebugNode/__init__.py +4 -0
  38. pynode/nodes/DebugNode/debug_node.py +169 -0
  39. pynode/nodes/DebugNode/requirements.txt +1 -0
  40. pynode/nodes/DelayNode/delay_node.py +218 -0
  41. pynode/nodes/DelayNode/requirements.txt +1 -0
  42. pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
  43. pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
  44. pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
  45. pynode/nodes/ErrorNode/error_node.py +94 -0
  46. pynode/nodes/ErrorNode/requirements.txt +1 -0
  47. pynode/nodes/FilterNode/__init__.py +3 -0
  48. pynode/nodes/FilterNode/filter_node.py +102 -0
  49. pynode/nodes/FilterNode/requirements.txt +1 -0
  50. pynode/nodes/FrameSourceNode/__init__.py +4 -0
  51. pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
  52. pynode/nodes/FrameSourceNode/requirements.txt +1 -0
  53. pynode/nodes/FunctionNode/__init__.py +3 -0
  54. pynode/nodes/FunctionNode/function_node.py +168 -0
  55. pynode/nodes/FunctionNode/requirements.txt +1 -0
  56. pynode/nodes/GateNode/gate_node.py +60 -0
  57. pynode/nodes/GateNode/requirements.txt +1 -0
  58. pynode/nodes/GetiUploadNode/__init__.py +3 -0
  59. pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
  60. pynode/nodes/GetiUploadNode/requirements.txt +1 -0
  61. pynode/nodes/ImageFormatNode/__init__.py +4 -0
  62. pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
  63. pynode/nodes/ImageFormatNode/requirements.txt +2 -0
  64. pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
  65. pynode/nodes/ImageViewerNode/__init__.py +4 -0
  66. pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
  67. pynode/nodes/ImageViewerNode/requirements.txt +2 -0
  68. pynode/nodes/ImageWriterNode/__init__.py +3 -0
  69. pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
  70. pynode/nodes/ImageWriterNode/requirements.txt +1 -0
  71. pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
  72. pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
  73. pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
  74. pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
  75. pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
  76. pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
  77. pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
  78. pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
  79. pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
  80. pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
  81. pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
  82. pynode/nodes/InferenceNode/inference_node.py +451 -0
  83. pynode/nodes/InferenceNode/requirements.txt +13 -0
  84. pynode/nodes/InjectNode/inject_node.py +245 -0
  85. pynode/nodes/InjectNode/requirements.txt +1 -0
  86. pynode/nodes/JoinNode/__init__.py +3 -0
  87. pynode/nodes/JoinNode/join_node.py +156 -0
  88. pynode/nodes/JoinNode/requirements.txt +1 -0
  89. pynode/nodes/LabelFilterNode/__init__.py +3 -0
  90. pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
  91. pynode/nodes/LabelFilterNode/requirements.txt +1 -0
  92. pynode/nodes/MQTTNode/__init__.py +8 -0
  93. pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
  94. pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
  95. pynode/nodes/MQTTNode/mqtt_service.py +367 -0
  96. pynode/nodes/MQTTNode/requirements.txt +1 -0
  97. pynode/nodes/MessageWriterNode/__init__.py +7 -0
  98. pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
  99. pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
  100. pynode/nodes/OmronCameraNode/__init__.py +3 -0
  101. pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
  102. pynode/nodes/OmronCameraNode/requirements.txt +4 -0
  103. pynode/nodes/OpenCV/__init__.py +1 -0
  104. pynode/nodes/OpenCV/arithmetic_node.py +376 -0
  105. pynode/nodes/OpenCV/bitwise_node.py +127 -0
  106. pynode/nodes/OpenCV/blend_node.py +127 -0
  107. pynode/nodes/OpenCV/blob_detector_node.py +233 -0
  108. pynode/nodes/OpenCV/blur_node.py +141 -0
  109. pynode/nodes/OpenCV/color_space_node.py +139 -0
  110. pynode/nodes/OpenCV/colormap_node.py +262 -0
  111. pynode/nodes/OpenCV/contours_node.py +258 -0
  112. pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
  113. pynode/nodes/OpenCV/draw_node.py +312 -0
  114. pynode/nodes/OpenCV/edge_detector_node.py +177 -0
  115. pynode/nodes/OpenCV/equirectangular_node.py +447 -0
  116. pynode/nodes/OpenCV/fft_node.py +102 -0
  117. pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
  118. pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
  119. pynode/nodes/OpenCV/histogram_node.py +160 -0
  120. pynode/nodes/OpenCV/hough_circles_node.py +224 -0
  121. pynode/nodes/OpenCV/hough_lines_node.py +243 -0
  122. pynode/nodes/OpenCV/in_range_node.py +186 -0
  123. pynode/nodes/OpenCV/morphology_node.py +136 -0
  124. pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
  125. pynode/nodes/OpenCV/paste_node.py +244 -0
  126. pynode/nodes/OpenCV/perspective_node.py +174 -0
  127. pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
  128. pynode/nodes/OpenCV/requirements.txt +2 -0
  129. pynode/nodes/OpenCV/resize_node.py +175 -0
  130. pynode/nodes/OpenCV/rotate_node.py +159 -0
  131. pynode/nodes/OpenCV/template_match_node.py +227 -0
  132. pynode/nodes/OpenCV/threshold_node.py +167 -0
  133. pynode/nodes/PointInShapeNode/__init__.py +3 -0
  134. pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
  135. pynode/nodes/PointInShapeNode/requirements.txt +2 -0
  136. pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
  137. pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
  138. pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
  139. pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
  140. pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
  141. pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
  142. pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
  143. pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
  144. pynode/nodes/RESTEndpointNode/__init__.py +3 -0
  145. pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
  146. pynode/nodes/RangeNode/__init__.py +3 -0
  147. pynode/nodes/RangeNode/range_node.py +115 -0
  148. pynode/nodes/RangeNode/requirements.txt +1 -0
  149. pynode/nodes/RateProbeNode/__init__.py +3 -0
  150. pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
  151. pynode/nodes/RateProbeNode/requirements.txt +1 -0
  152. pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
  153. pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
  154. pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
  155. pynode/nodes/SliceNode/README.md +149 -0
  156. pynode/nodes/SliceNode/__init__.py +5 -0
  157. pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
  158. pynode/nodes/SliceNode/requirements.txt +3 -0
  159. pynode/nodes/SliceNode/slice_collector_node.py +370 -0
  160. pynode/nodes/SliceNode/slice_image_node.py +380 -0
  161. pynode/nodes/SplitNode/__init__.py +3 -0
  162. pynode/nodes/SplitNode/requirements.txt +1 -0
  163. pynode/nodes/SplitNode/split_node.py +155 -0
  164. pynode/nodes/SwitchNode/requirements.txt +1 -0
  165. pynode/nodes/SwitchNode/switch_node.py +259 -0
  166. pynode/nodes/SyncNode/__init__.py +3 -0
  167. pynode/nodes/SyncNode/sync_node.py +287 -0
  168. pynode/nodes/TemplateNode/requirements.txt +1 -0
  169. pynode/nodes/TemplateNode/template_node.py +86 -0
  170. pynode/nodes/TrackerNode/__init__.py +2 -0
  171. pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
  172. pynode/nodes/TrackerNode/tracker_node.py +745 -0
  173. pynode/nodes/UltralyticsNode/__init__.py +4 -0
  174. pynode/nodes/UltralyticsNode/requirements.txt +2 -0
  175. pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
  176. pynode/nodes/UnknownNode/__init__.py +93 -0
  177. pynode/nodes/VLLMNode/__init__.py +3 -0
  178. pynode/nodes/VLLMNode/vllm_node.py +345 -0
  179. pynode/nodes/VideoReaderNode/__init__.py +3 -0
  180. pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
  181. pynode/nodes/VideoWriterNode/__init__.py +3 -0
  182. pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
  183. pynode/nodes/WebhookNode/__init__.py +3 -0
  184. pynode/nodes/WebhookNode/webhook_node.py +591 -0
  185. pynode/nodes/__init__.py +109 -0
  186. pynode/nodes/base_node.py +562 -0
  187. pynode/nodes/image_utils.py +291 -0
  188. pynode/nodes/info.py +109 -0
  189. pynode/nodes/mDNSNode/__init__.py +13 -0
  190. pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
  191. pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
  192. pynode/nodes/mDNSNode/mdns_manager.py +399 -0
  193. pynode/nodes/mDNSNode/requirements.txt +1 -0
  194. pynode/nodes/messages.py +81 -0
  195. pynode/server.py +187 -0
  196. pynode/static/images/favicon4.png +0 -0
  197. pynode/static/index.html +200 -0
  198. pynode/static/js/README.md +139 -0
  199. pynode/static/js/auth.js +89 -0
  200. pynode/static/js/clipboard.js +152 -0
  201. pynode/static/js/config.js +14 -0
  202. pynode/static/js/connections.js +844 -0
  203. pynode/static/js/debug.js +524 -0
  204. pynode/static/js/events.js +775 -0
  205. pynode/static/js/history.js +199 -0
  206. pynode/static/js/main.js +55 -0
  207. pynode/static/js/minimap.js +242 -0
  208. pynode/static/js/nodes.js +995 -0
  209. pynode/static/js/palette.js +255 -0
  210. pynode/static/js/properties.js +1270 -0
  211. pynode/static/js/selection.js +241 -0
  212. pynode/static/js/state.js +211 -0
  213. pynode/static/js/ui-utils.js +28 -0
  214. pynode/static/js/viewport.js +204 -0
  215. pynode/static/js/workflow.js +469 -0
  216. pynode/static/js/workflows.js +344 -0
  217. pynode/static/style.css +2751 -0
  218. pynode/workflow_engine.py +483 -0
  219. pynode/workflow_manager.py +440 -0
  220. pynode_flow-0.2.0.dist-info/METADATA +520 -0
  221. pynode_flow-0.2.0.dist-info/RECORD +227 -0
  222. pynode_flow-0.2.0.dist-info/WHEEL +5 -0
  223. pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
  224. pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
  225. pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
  226. pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
  227. pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,255 @@
1
+ // Node palette rendering
2
+ import { API_BASE, NODE_CATEGORIES } from './config.js';
3
+ import { state, setNodeTypes } from './state.js';
4
+ import { categoryLabel } from './ui-utils.js';
5
+
6
+ // Collapsed-state of categories, keyed by lowercase category value and
7
+ // persisted in localStorage. On very first load (no stored state) large
8
+ // categories (> LARGE_CATEGORY_THRESHOLD nodes, e.g. OpenCV) start collapsed.
9
+ const COLLAPSED_STORAGE_KEY = 'pynode.palette.collapsedCategories';
10
+ const LARGE_CATEGORY_THRESHOLD = 10;
11
+ let collapsedCategories = null; // Set of lowercase category keys (lazy init)
12
+
13
+ function initCollapsedCategories(categories) {
14
+ if (collapsedCategories) return;
15
+
16
+ const stored = localStorage.getItem(COLLAPSED_STORAGE_KEY);
17
+ if (stored !== null) {
18
+ try {
19
+ collapsedCategories = new Set(JSON.parse(stored));
20
+ return;
21
+ } catch (e) {
22
+ console.warn('Ignoring corrupt palette collapse state:', e);
23
+ }
24
+ }
25
+
26
+ // First load: collapse large categories by default
27
+ collapsedCategories = new Set();
28
+ Object.entries(categories).forEach(([key, category]) => {
29
+ if (category.nodes.length > LARGE_CATEGORY_THRESHOLD) {
30
+ collapsedCategories.add(key);
31
+ }
32
+ });
33
+ saveCollapsedCategories();
34
+ }
35
+
36
+ function saveCollapsedCategories() {
37
+ localStorage.setItem(COLLAPSED_STORAGE_KEY, JSON.stringify([...collapsedCategories]));
38
+ }
39
+
40
+ export async function loadNodeTypes() {
41
+ try {
42
+ const response = await fetch(`${API_BASE}/node-types`);
43
+ const types = await response.json();
44
+ setNodeTypes(types); // Use the setter to build both array and map
45
+ renderNodePalette();
46
+ setupPaletteSearch();
47
+ } catch (error) {
48
+ console.error('Failed to load node types:', error);
49
+ }
50
+ }
51
+
52
+ function setupPaletteSearch() {
53
+ const searchInput = document.getElementById('palette-search');
54
+ const clearBtn = document.getElementById('palette-clear-btn');
55
+ if (!searchInput) return;
56
+
57
+ const updateClearVisibility = () => {
58
+ if (!clearBtn) return;
59
+ clearBtn.style.display = searchInput.value.trim() ? 'block' : 'none';
60
+ };
61
+
62
+ searchInput.addEventListener('input', (e) => {
63
+ const filter = e.target.value.toLowerCase().trim();
64
+ filterPaletteNodes(filter);
65
+ updateClearVisibility();
66
+ });
67
+
68
+ if (clearBtn) {
69
+ clearBtn.addEventListener('click', (e) => {
70
+ e.preventDefault();
71
+ searchInput.value = '';
72
+ filterPaletteNodes('');
73
+ updateClearVisibility();
74
+ searchInput.focus();
75
+ });
76
+ // Initialize visibility
77
+ updateClearVisibility();
78
+ }
79
+ }
80
+
81
+ function filterPaletteNodes(filter) {
82
+ const palette = document.getElementById('node-palette');
83
+ const categories = palette.querySelectorAll('.palette-category');
84
+
85
+ categories.forEach(category => {
86
+ const nodes = category.querySelectorAll('.palette-node');
87
+ let visibleCount = 0;
88
+
89
+ nodes.forEach(node => {
90
+ const nodeName = node.querySelector('.palette-node-name').textContent.toLowerCase();
91
+ if (!filter || nodeName.includes(filter)) {
92
+ node.style.display = '';
93
+ visibleCount++;
94
+ } else {
95
+ node.style.display = 'none';
96
+ }
97
+ });
98
+
99
+ // Hide category if no nodes match
100
+ category.style.display = visibleCount > 0 ? '' : 'none';
101
+ });
102
+ }
103
+
104
+ export function renderNodePalette() {
105
+ const palette = document.getElementById('node-palette');
106
+ palette.innerHTML = '';
107
+
108
+ // Dynamically group nodes by category (case-insensitively, so 'opencv'
109
+ // and 'OpenCV' land in the same bucket), preserving the server-provided
110
+ // order of first appearance.
111
+ const categories = {};
112
+ state.nodeTypes.forEach(nodeType => {
113
+ const key = String(nodeType.category || 'custom').toLowerCase();
114
+ if (!categories[key]) {
115
+ categories[key] = {
116
+ key: key,
117
+ title: categoryLabel(key),
118
+ nodes: []
119
+ };
120
+ }
121
+ categories[key].nodes.push(nodeType);
122
+ });
123
+
124
+ initCollapsedCategories(categories);
125
+
126
+ // Render each category in order of appearance
127
+ Object.values(categories).forEach(category => {
128
+ if (category.nodes.length === 0) return;
129
+
130
+ const categoryEl = document.createElement('div');
131
+ categoryEl.className = 'palette-category';
132
+
133
+ const headerEl = document.createElement('div');
134
+ headerEl.className = 'palette-category-header';
135
+
136
+ // Add collapse arrow
137
+ const arrowEl = document.createElement('span');
138
+ arrowEl.className = 'palette-category-arrow';
139
+ arrowEl.textContent = '▼';
140
+ if (collapsedCategories.has(category.key)) {
141
+ arrowEl.classList.add('collapsed');
142
+ }
143
+ headerEl.appendChild(arrowEl);
144
+
145
+ const titleEl = document.createElement('span');
146
+ titleEl.textContent = category.title;
147
+ headerEl.appendChild(titleEl);
148
+
149
+ // Add node count badge
150
+ const countEl = document.createElement('span');
151
+ countEl.className = 'palette-category-count';
152
+ countEl.textContent = category.nodes.length;
153
+ headerEl.appendChild(countEl);
154
+
155
+ categoryEl.appendChild(headerEl);
156
+
157
+ const listEl = document.createElement('div');
158
+ listEl.className = 'palette-category-list';
159
+
160
+ // Apply collapsed state
161
+ if (collapsedCategories.has(category.key)) {
162
+ listEl.classList.add('collapsed');
163
+ arrowEl.classList.add('collapsed');
164
+ }
165
+
166
+ // Toggle collapse on header click
167
+ headerEl.addEventListener('click', () => {
168
+ const isCollapsed = listEl.classList.contains('collapsed');
169
+
170
+ if (isCollapsed) {
171
+ // Expand: set max-height to scrollHeight for animation
172
+ listEl.style.maxHeight = listEl.scrollHeight + 'px';
173
+ listEl.classList.remove('collapsed');
174
+ arrowEl.classList.remove('collapsed');
175
+ collapsedCategories.delete(category.key);
176
+
177
+ // After animation, remove max-height to allow dynamic content
178
+ setTimeout(() => {
179
+ if (!listEl.classList.contains('collapsed')) {
180
+ listEl.style.maxHeight = 'none';
181
+ }
182
+ }, 250);
183
+ } else {
184
+ // Collapse: first set explicit max-height, then collapse
185
+ listEl.style.maxHeight = listEl.scrollHeight + 'px';
186
+ // Force reflow
187
+ listEl.offsetHeight;
188
+ listEl.classList.add('collapsed');
189
+ arrowEl.classList.add('collapsed');
190
+ collapsedCategories.add(category.key);
191
+ }
192
+ saveCollapsedCategories();
193
+ });
194
+
195
+ category.nodes.forEach(nodeType => {
196
+ const nodeEl = document.createElement('div');
197
+ nodeEl.className = 'palette-node';
198
+
199
+ const icon = nodeType.icon || '◆';
200
+ const inputCount = nodeType.inputCount !== undefined ? nodeType.inputCount : 1;
201
+ const outputCount = nodeType.outputCount !== undefined ? nodeType.outputCount : 1;
202
+
203
+ const portsHtml = `
204
+ <div class="palette-node-ports">
205
+ ${inputCount > 0 ? '<div class="palette-port input"></div>' : ''}
206
+ ${outputCount > 0 ? '<div class="palette-port output"></div>' : ''}
207
+ </div>
208
+ `;
209
+
210
+ nodeEl.innerHTML = `
211
+ <span class="palette-node-icon">${icon}</span>
212
+ <span class="palette-node-name">${nodeType.name}</span>
213
+ ${portsHtml}
214
+ `;
215
+ nodeEl.draggable = true;
216
+
217
+ // Apply node colors
218
+ if (nodeType.color) nodeEl.style.backgroundColor = nodeType.color;
219
+ if (nodeType.borderColor) nodeEl.style.borderColor = nodeType.borderColor;
220
+ if (nodeType.textColor) nodeEl.style.color = nodeType.textColor;
221
+
222
+ nodeEl.addEventListener('dragstart', (e) => {
223
+ e.dataTransfer.setData('nodeType', nodeType.type);
224
+
225
+ // Center the node on the pointer
226
+ const rect = nodeEl.getBoundingClientRect();
227
+ const offsetX = rect.width / 2;
228
+ const offsetY = rect.height / 2;
229
+ e.dataTransfer.setData('dragOffsetX', offsetX.toString());
230
+ e.dataTransfer.setData('dragOffsetY', offsetY.toString());
231
+
232
+ // Create a custom drag image that isn't faded
233
+ const dragImage = nodeEl.cloneNode(true);
234
+ dragImage.style.position = 'absolute';
235
+ dragImage.style.top = '-1000px';
236
+ dragImage.style.left = '-1000px';
237
+ dragImage.style.opacity = '1';
238
+ dragImage.style.transform = 'none';
239
+ dragImage.style.pointerEvents = 'none';
240
+ document.body.appendChild(dragImage);
241
+ e.dataTransfer.setDragImage(dragImage, offsetX, offsetY);
242
+
243
+ // Clean up the drag image after a short delay
244
+ setTimeout(() => {
245
+ document.body.removeChild(dragImage);
246
+ }, 0);
247
+ });
248
+
249
+ listEl.appendChild(nodeEl);
250
+ });
251
+
252
+ categoryEl.appendChild(listEl);
253
+ palette.appendChild(categoryEl);
254
+ });
255
+ }