broadfield-dev commited on
Commit
5d1d174
·
verified ·
1 Parent(s): 837c995

Update static/canvas.js

Browse files
Files changed (1) hide show
  1. static/canvas.js +225 -41
static/canvas.js CHANGED
@@ -3,7 +3,7 @@ const stage = new Konva.Stage({
3
  container: 'container',
4
  width: 1000,
5
  height: 600,
6
- draggable: true // Enable stage dragging for panning
7
  });
8
 
9
  const layer = new Konva.Layer();
@@ -11,10 +11,11 @@ stage.add(layer);
11
 
12
  // Store nodes, connections, and parsed data
13
  let nodes = [];
14
- let connections = []; // { fromNodeId, fromPortId, toNodeId, toPortId }
15
  let parsedConnections = [];
16
- let selectedPort = null; // { node, portId, type: 'input'|'output' }
17
  let disconnectMode = false;
 
18
 
19
  // Zoom functionality
20
  let scale = 1;
@@ -23,28 +24,18 @@ const minScale = 0.5;
23
  const maxScale = 2.0;
24
 
25
  stage.on('wheel', (e) => {
26
- e.evt.preventDefault(); // Prevent default browser scrolling
27
-
28
  const oldScale = scale;
29
  const pointer = stage.getPointerPosition();
30
-
31
- // Determine zoom direction
32
  const delta = e.evt.deltaY > 0 ? 1 / scaleFactor : scaleFactor;
33
  const newScale = Math.max(minScale, Math.min(maxScale, oldScale * delta));
34
-
35
- if (newScale === scale) return; // No change in scale
36
  scale = newScale;
37
-
38
- // Calculate mouse position relative to stage
39
  const mousePointTo = {
40
  x: (pointer.x - stage.x()) / oldScale,
41
  y: (pointer.y - stage.y()) / oldScale
42
  };
43
-
44
- // Apply new scale
45
  stage.scale({ x: scale, y: scale });
46
-
47
- // Adjust stage position to keep the zoom centered on the mouse
48
  const newPos = {
49
  x: pointer.x - mousePointTo.x * scale,
50
  y: pointer.y - mousePointTo.y * scale
@@ -79,7 +70,7 @@ function submitCode() {
79
  return;
80
  }
81
  clearCanvas();
82
- parsedConnections = data.connections; // Store for auto-connect
83
  createNodesFromParsedData(data.nodes, data.connections);
84
  })
85
  .catch(error => console.error('Error:', error));
@@ -92,6 +83,10 @@ function clearCanvas() {
92
  nodes = [];
93
  connections = [];
94
  parsedConnections = [];
 
 
 
 
95
  layer.draw();
96
  }
97
 
@@ -105,17 +100,21 @@ function createNodesFromParsedData(parsedNodes, parsedConnections) {
105
  nodeData.type,
106
  nodeData.inputs,
107
  nodeData.outputs,
108
- nodeData.id
 
 
 
109
  );
110
  nodes.push(node);
111
  layer.add(node);
112
  });
113
  layer.draw();
 
114
  saveNodes();
115
  }
116
 
117
- // Create a node with inputs and outputs
118
- function createNode(x, y, label, type, inputs = [], outputs = [], id) {
119
  const node = new Konva.Group({
120
  x: x,
121
  y: y,
@@ -123,10 +122,14 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
123
  });
124
 
125
  // Node rectangle
126
- const color = type === 'function' ? '#ffeb3b' : type.includes('variable') ? '#90caf9' : type === 'import' ? '#a5d6a7' : '#ccc';
 
 
 
 
127
  const box = new Konva.Rect({
128
- width: 100,
129
- height: 50,
130
  fill: color,
131
  stroke: 'black',
132
  strokeWidth: 2,
@@ -139,15 +142,15 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
139
  fontSize: 12,
140
  fontFamily: 'Arial',
141
  fill: 'black',
142
- width: 100,
143
  align: 'center',
144
- y: 20
145
  });
146
 
147
  node.add(box);
148
  node.add(text);
149
 
150
- // Add input/output ports with unique IDs
151
  const inputPorts = inputs.map((input, i) => ({
152
  id: `input-${id}-${i}`,
153
  name: input,
@@ -163,7 +166,7 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
163
  id: `output-${id}-${i}`,
164
  name: output,
165
  circle: new Konva.Circle({
166
- x: 100,
167
  y: 10 + i * 20,
168
  radius: 5,
169
  fill: 'green'
@@ -176,9 +179,8 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
176
  port.circle.on('click', () => {
177
  if (!selectedPort) {
178
  selectedPort = { node, portId: port.id, type: 'input' };
179
- disconnectMode = true; // First click on input enables disconnect mode
180
  } else if (selectedPort.type === 'output' && selectedPort.node !== node) {
181
- // Connect output to input
182
  createSplineConnection(
183
  selectedPort.node,
184
  selectedPort.portId,
@@ -195,8 +197,7 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
195
  disconnectMode = false;
196
  saveNodes();
197
  } else if (disconnectMode && selectedPort.type === 'input' && selectedPort.node === node) {
198
- // Select output to disconnect
199
- selectedPort = { node, portId: port.id, type: 'input' }; // Keep input selected
200
  } else {
201
  selectedPort = null;
202
  disconnectMode = false;
@@ -211,7 +212,6 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
211
  selectedPort = { node, portId: port.id, type: 'output' };
212
  disconnectMode = false;
213
  } else if (disconnectMode && selectedPort.type === 'input') {
214
- // Disconnect input from output
215
  const connIndex = connections.findIndex(
216
  c => c.toNodeId === selectedPort.node.data.id &&
217
  c.toPortId === selectedPort.portId &&
@@ -220,7 +220,7 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
220
  );
221
  if (connIndex !== -1) {
222
  const conn = connections[connIndex];
223
- const spline = layer.find('Shape').find(s =>
224
  s.data.fromNodeId === conn.fromNodeId &&
225
  s.data.fromPortId === conn.fromPortId &&
226
  s.data.toNodeId === conn.toNodeId &&
@@ -241,20 +241,103 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
241
  });
242
 
243
  // Node data
244
- node.data = {
245
  id: id,
246
  type: type,
247
  label: label,
248
  inputs: inputPorts,
249
  outputs: outputPorts,
250
  x: x,
251
- y: y
 
 
 
252
  };
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  // Update position and connections on drag
255
  node.on('dragmove', () => {
256
  node.data.x = node.x();
257
  node.data.y = node.y();
 
 
 
 
 
 
 
 
 
258
  updateConnections();
259
  saveNodes();
260
  });
@@ -262,7 +345,7 @@ function createNode(x, y, label, type, inputs = [], outputs = [], id) {
262
  return node;
263
  }
264
 
265
- // Create a spline (Bezier curve) connection
266
  function createSplineConnection(fromNode, fromPortId, toNode, toPortId) {
267
  const fromPort = fromNode.data.outputs.find(p => p.id === fromPortId);
268
  const toPort = toNode.data.inputs.find(p => p.id === toPortId);
@@ -273,7 +356,6 @@ function createSplineConnection(fromNode, fromPortId, toNode, toPortId) {
273
  const endX = toNode.x() + toPort.circle.x();
274
  const endY = toNode.y() + toPort.circle.y();
275
 
276
- // Control points for Bezier curve
277
  const control1X = startX + (endX - startX) / 3;
278
  const control1Y = startY;
279
  const control2X = startX + 2 * (endX - startX) / 3;
@@ -300,7 +382,7 @@ function createSplineConnection(fromNode, fromPortId, toNode, toPortId) {
300
  layer.draw();
301
  }
302
 
303
- // Auto-connect nodes based on parsed connections
304
  function autoConnect() {
305
  // Clear existing connections
306
  layer.find('Shape').forEach(shape => {
@@ -310,12 +392,25 @@ function autoConnect() {
310
  });
311
  connections = [];
312
 
313
- // Create spline connections for parsed data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  parsedConnections.forEach(conn => {
315
  const fromNode = nodes.find(n => n.data.id === conn.from);
316
  const toNode = nodes.find(n => n.data.id === conn.to);
317
  if (fromNode && toNode) {
318
- // Find first available output and input ports
319
  const fromPort = fromNode.data.outputs[0];
320
  const toPort = toNode.data.inputs[0];
321
  if (fromPort && toPort) {
@@ -330,10 +425,93 @@ function autoConnect() {
330
  }
331
  });
332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  layer.draw();
334
  saveNodes();
335
  }
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  // Add a manual node
338
  function addNode() {
339
  const node = createNode(
@@ -343,7 +521,10 @@ function addNode() {
343
  'function',
344
  ['in1'],
345
  ['out1'],
346
- nodes.length
 
 
 
347
  );
348
  nodes.push(node);
349
  layer.add(node);
@@ -399,7 +580,10 @@ function saveNodes() {
399
  x: n.data.x,
400
  y: n.data.y,
401
  inputs: n.data.inputs.map(p => p.name),
402
- outputs: n.data.outputs.map(p => p.name)
 
 
 
403
  })),
404
  connections: connections
405
  })
 
3
  container: 'container',
4
  width: 1000,
5
  height: 600,
6
+ draggable: true
7
  });
8
 
9
  const layer = new Konva.Layer();
 
11
 
12
  // Store nodes, connections, and parsed data
13
  let nodes = [];
14
+ let connections = [];
15
  let parsedConnections = [];
16
+ let selectedPort = null;
17
  let disconnectMode = false;
18
+ let codeWindow = null; // Track open code window
19
 
20
  // Zoom functionality
21
  let scale = 1;
 
24
  const maxScale = 2.0;
25
 
26
  stage.on('wheel', (e) => {
27
+ e.evt.preventDefault();
 
28
  const oldScale = scale;
29
  const pointer = stage.getPointerPosition();
 
 
30
  const delta = e.evt.deltaY > 0 ? 1 / scaleFactor : scaleFactor;
31
  const newScale = Math.max(minScale, Math.min(maxScale, oldScale * delta));
32
+ if (newScale === scale) return;
 
33
  scale = newScale;
 
 
34
  const mousePointTo = {
35
  x: (pointer.x - stage.x()) / oldScale,
36
  y: (pointer.y - stage.y()) / oldScale
37
  };
 
 
38
  stage.scale({ x: scale, y: scale });
 
 
39
  const newPos = {
40
  x: pointer.x - mousePointTo.x * scale,
41
  y: pointer.y - mousePointTo.y * scale
 
70
  return;
71
  }
72
  clearCanvas();
73
+ parsedConnections = data.connections;
74
  createNodesFromParsedData(data.nodes, data.connections);
75
  })
76
  .catch(error => console.error('Error:', error));
 
83
  nodes = [];
84
  connections = [];
85
  parsedConnections = [];
86
+ if (codeWindow) {
87
+ codeWindow.destroy();
88
+ codeWindow = null;
89
+ }
90
  layer.draw();
91
  }
92
 
 
100
  nodeData.type,
101
  nodeData.inputs,
102
  nodeData.outputs,
103
+ nodeData.id,
104
+ nodeData.source,
105
+ nodeData.parent_path,
106
+ nodeData.level
107
  );
108
  nodes.push(node);
109
  layer.add(node);
110
  });
111
  layer.draw();
112
+ autoConnect(); // Call autoConnect after nodes are created
113
  saveNodes();
114
  }
115
 
116
+ // Create a node with inputs, outputs, and code segment
117
+ function createNode(x, y, label, type, inputs = [], outputs = [], id, source = '', parent_path = '', level = 0) {
118
  const node = new Konva.Group({
119
  x: x,
120
  y: y,
 
122
  });
123
 
124
  // Node rectangle
125
+ const isNumberBox = type === 'number_box';
126
+ const color = isNumberBox ? '#ffcccb' : type === 'function' ? '#ffeb3b' : type.includes('variable') ? '#90caf9' : type === 'import' ? '#a5d6a7' : '#ccc';
127
+ const width = isNumberBox ? 80 : 100;
128
+ const height = isNumberBox ? 40 : 50;
129
+
130
  const box = new Konva.Rect({
131
+ width: width,
132
+ height: height,
133
  fill: color,
134
  stroke: 'black',
135
  strokeWidth: 2,
 
142
  fontSize: 12,
143
  fontFamily: 'Arial',
144
  fill: 'black',
145
+ width: width,
146
  align: 'center',
147
+ y: isNumberBox ? 14 : 20
148
  });
149
 
150
  node.add(box);
151
  node.add(text);
152
 
153
+ // Input/output ports
154
  const inputPorts = inputs.map((input, i) => ({
155
  id: `input-${id}-${i}`,
156
  name: input,
 
166
  id: `output-${id}-${i}`,
167
  name: output,
168
  circle: new Konva.Circle({
169
+ x: width,
170
  y: 10 + i * 20,
171
  radius: 5,
172
  fill: 'green'
 
179
  port.circle.on('click', () => {
180
  if (!selectedPort) {
181
  selectedPort = { node, portId: port.id, type: 'input' };
182
+ disconnectMode = true;
183
  } else if (selectedPort.type === 'output' && selectedPort.node !== node) {
 
184
  createSplineConnection(
185
  selectedPort.node,
186
  selectedPort.portId,
 
197
  disconnectMode = false;
198
  saveNodes();
199
  } else if (disconnectMode && selectedPort.type === 'input' && selectedPort.node === node) {
200
+ selectedPort = { node, portId: port.id, type: 'input' };
 
201
  } else {
202
  selectedPort = null;
203
  disconnectMode = false;
 
212
  selectedPort = { node, portId: port.id, type: 'output' };
213
  disconnectMode = false;
214
  } else if (disconnectMode && selectedPort.type === 'input') {
 
215
  const connIndex = connections.findIndex(
216
  c => c.toNodeId === selectedPort.node.data.id &&
217
  c.toPortId === selectedPort.portId &&
 
220
  );
221
  if (connIndex !== -1) {
222
  const conn = connections[connIndex];
223
+ const spline = layer.find('Shape').find(s =>
224
  s.data.fromNodeId === conn.fromNodeId &&
225
  s.data.fromPortId === conn.fromPortId &&
226
  s.data.toNodeId === conn.toNodeId &&
 
241
  });
242
 
243
  // Node data
244
+ nodeintersectionObserver.data = {
245
  id: id,
246
  type: type,
247
  label: label,
248
  inputs: inputPorts,
249
  outputs: outputPorts,
250
  x: x,
251
+ y: y,
252
+ source: source,
253
+ parent_path: parent_path,
254
+ level: level
255
  };
256
 
257
+ // Click handler to show code window
258
+ box.on('click', () => {
259
+ if (codeWindow) {
260
+ codeWindow.destroy();
261
+ codeWindow = null;
262
+ }
263
+
264
+ const nodePos = node.getAbsolutePosition();
265
+ codeWindow = new Konva.Group({
266
+ x: nodePos.x,
267
+ y: nodePos.y + height + 10
268
+ });
269
+
270
+ const codeBox = new Konva.Rect({
271
+ width: 300,
272
+ height: 100,
273
+ fill: '#f0f0f0',
274
+ stroke: 'black',
275
+ strokeWidth: 1,
276
+ cornerRadius: 5
277
+ });
278
+
279
+ const textarea = document.createElement('textarea');
280
+ textarea.style.position = 'absolute';
281
+ textarea.style.left = `${nodePos.x / scale + stage.x() / scale}px`;
282
+ textarea.style.top = `${(nodePos.y + height + 10) / scale + stage.y() / scale}px`;
283
+ textarea.style.width = '300px';
284
+ textarea.style.height = '100px';
285
+ textarea.style.fontFamily = 'monospace';
286
+ textarea.value = source || '';
287
+ document.body.appendChild(textarea);
288
+
289
+ // Update code on change
290
+ textarea.addEventListener('change', () => {
291
+ const newSource = textarea.value;
292
+ node.data.source = newSource;
293
+ fetch('/update_node', {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/json' },
296
+ body: JSON.stringify({
297
+ id: node.data.id,
298
+ source: newSource
299
+ })
300
+ })
301
+ .then(response => response.json())
302
+ .then(data => {
303
+ if (data.error) {
304
+ alert(data.error);
305
+ } else {
306
+ console.log('Node updated:', data);
307
+ updateProgram();
308
+ }
309
+ })
310
+ .catch(error => console.error('Error:', error));
311
+ });
312
+
313
+ // Close window on click outside
314
+ stage.on('click', (e) => {
315
+ if (e.target !== box && codeWindow) {
316
+ codeWindow.destroy();
317
+ document.body.removeChild(textarea);
318
+ codeWindow = null;
319
+ stage.off('click');
320
+ }
321
+ });
322
+
323
+ codeWindow.add(codeBox);
324
+ layer.add(codeWindow);
325
+ layer.draw();
326
+ });
327
+
328
  // Update position and connections on drag
329
  node.on('dragmove', () => {
330
  node.data.x = node.x();
331
  node.data.y = node.y();
332
+ if (codeWindow) {
333
+ const nodePos = node.getAbsolutePosition();
334
+ codeWindow.position({ x: nodePos.x, y: nodePos.y + height + 10 });
335
+ const textarea = document.querySelector(`textarea`);
336
+ if (textarea) {
337
+ textarea.style.left = `${nodePos.x / scale + stage.x() / scale}px`;
338
+ textarea.style.top = `${(nodePos.y + height + 10) / scale + stage.y() / scale}px`;
339
+ }
340
+ }
341
  updateConnections();
342
  saveNodes();
343
  });
 
345
  return node;
346
  }
347
 
348
+ // Create a spline connection
349
  function createSplineConnection(fromNode, fromPortId, toNode, toPortId) {
350
  const fromPort = fromNode.data.outputs.find(p => p.id === fromPortId);
351
  const toPort = toNode.data.inputs.find(p => p.id === toPortId);
 
356
  const endX = toNode.x() + toPort.circle.x();
357
  const endY = toNode.y() + toPort.circle.y();
358
 
 
359
  const control1X = startX + (endX - startX) / 3;
360
  const control1Y = startY;
361
  const control2X = startX + 2 * (endX - startX) / 3;
 
382
  layer.draw();
383
  }
384
 
385
+ // Enhanced auto-connect based on hierarchy, position, and role
386
  function autoConnect() {
387
  // Clear existing connections
388
  layer.find('Shape').forEach(shape => {
 
392
  });
393
  connections = [];
394
 
395
+ // Sort nodes by level and y-position to approximate program order
396
+ const sortedNodes = [...nodes].sort((a, b) => {
397
+ if (a.data.level !== b.data.level) return a.data.level - b.data.level;
398
+ return a.data.y - b.data.y;
399
+ });
400
+
401
+ // Build hierarchy map
402
+ const hierarchy = {};
403
+ sortedNodes.forEach(node => {
404
+ const parent = node.data.parent_path.split(' -> ')[0] || 'global';
405
+ if (!hierarchy[parent]) hierarchy[parent] = [];
406
+ hierarchy[parent].push(node);
407
+ });
408
+
409
+ // Create connections based on parsed connections and hierarchy
410
  parsedConnections.forEach(conn => {
411
  const fromNode = nodes.find(n => n.data.id === conn.from);
412
  const toNode = nodes.find(n => n.data.id === conn.to);
413
  if (fromNode && toNode) {
 
414
  const fromPort = fromNode.data.outputs[0];
415
  const toPort = toNode.data.inputs[0];
416
  if (fromPort && toPort) {
 
425
  }
426
  });
427
 
428
+ // Additional connections based on hierarchy and role
429
+ sortedNodes.forEach(node => {
430
+ const nodeId = node.data.id;
431
+ const parent = node.data.parent_path.split(' -> ')[0] || 'global';
432
+ const role = node.data.type;
433
+
434
+ // Connect to parent if applicable
435
+ if (parent !== 'global') {
436
+ const parentNode = nodes.find(n => n.data.id === parent || n.data.label === parent.split('[')[0]);
437
+ if (parentNode && parentNode.data.outputs.length > 0 && node.data.inputs.length > 0) {
438
+ const fromPort = parentNode.data.outputs[0];
439
+ const toPort = node.data.inputs[0];
440
+ if (!connections.some(c => c.fromNodeId === parentNode.data.id && c.toNodeId === nodeId)) {
441
+ createSplineConnection(parentNode, fromPort.id, node, toPort.id);
442
+ connections.push({
443
+ fromNodeId: parentNode.data.id,
444
+ fromPortId: fromPort.id,
445
+ toNodeId: nodeId,
446
+ toPortId: toPort.id
447
+ });
448
+ }
449
+ }
450
+ }
451
+
452
+ // Connect variables to their uses
453
+ if (role.includes('variable')) {
454
+ const varName = node.data.label;
455
+ sortedNodes.forEach(otherNode => {
456
+ if (otherNode !== node && otherNode.data.source.includes(varName)) {
457
+ const fromPort = node.data.outputs[0];
458
+ const toPort = otherNode.data.inputs[0];
459
+ if (fromPort && toPort && !connections.some(c => c.fromNodeId === nodeId && c.toNodeId === otherNode.data.id)) {
460
+ createSplineConnection(node, fromPort.id, otherNode, toPort.id);
461
+ connections.push({
462
+ fromNodeId: nodeId,
463
+ fromPortId: fromPort.id,
464
+ toNodeId: otherNode.data.id,
465
+ toPortId: toPort.id
466
+ });
467
+ }
468
+ }
469
+ });
470
+ }
471
+ });
472
+
473
  layer.draw();
474
  saveNodes();
475
  }
476
 
477
+ // Update full program
478
+ function updateProgram() {
479
+ const program = reconstructProgram();
480
+ document.getElementById('codeInput').value = program;
481
+ fetch('/update_program', {
482
+ method: 'POST',
483
+ headers: { 'Content-Type': 'application/json' },
484
+ body: JSON.stringify({ code: program })
485
+ })
486
+ .then(response => response.json())
487
+ .then(data => {
488
+ if (data.error) {
489
+ alert(data.error);
490
+ } else {
491
+ console.log('Program updated:', data);
492
+ }
493
+ })
494
+ .catch(error => console.error('Error:', error));
495
+ }
496
+
497
+ // Reconstruct the original program
498
+ function reconstructProgram() {
499
+ const sortedNodes = [...nodes].sort((a, b) => {
500
+ if (a.data.level !== b.data.level) return a.data.level - b.data.level;
501
+ return a.data.y - b.data.y;
502
+ });
503
+
504
+ let program = '';
505
+ sortedNodes.forEach(node => {
506
+ const source = node.data.source || '';
507
+ const level = node.data.level || 0;
508
+ const indent = ' '.repeat(level);
509
+ program += indent + source.trim() + '\n';
510
+ });
511
+
512
+ return program.trim();
513
+ }
514
+
515
  // Add a manual node
516
  function addNode() {
517
  const node = createNode(
 
521
  'function',
522
  ['in1'],
523
  ['out1'],
524
+ nodes.length,
525
+ 'def new_function(): pass',
526
+ 'global',
527
+ 0
528
  );
529
  nodes.push(node);
530
  layer.add(node);
 
580
  x: n.data.x,
581
  y: n.data.y,
582
  inputs: n.data.inputs.map(p => p.name),
583
+ outputs: n.data.outputs.map(p => p.name),
584
+ source: n.data.source,
585
+ parent_path: n.data.parent_path,
586
+ level: n.data.level
587
  })),
588
  connections: connections
589
  })