noding / static /canvas.js
broadfield-dev's picture
Update static/canvas.js
58dff6d verified
// Initialize Konva stage
const stage = new Konva.Stage({
container: 'container',
width: 1000,
height: 600,
draggable: true
});
const layer = new Konva.Layer();
stage.add(layer);
// Store nodes, connections, and parsed data
let nodes = [];
let connections = [];
let parsedConnections = [];
let selectedPort = null;
let disconnectMode = false;
let codeWindow = null; // Track open code window
let codeTextarea = null; // Track textarea element
// Zoom functionality
let scale = 1;
const scaleFactor = 1.1;
const minScale = 0.5;
const maxScale = 2.0;
stage.on('wheel', (e) => {
e.evt.preventDefault();
const oldScale = scale;
const pointer = stage.getPointerPosition();
const delta = e.evt.deltaY > 0 ? 1 / scaleFactor : scaleFactor;
const newScale = Math.max(minScale, Math.min(maxScale, oldScale * delta));
if (newScale === scale) return;
scale = newScale;
const mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale
};
stage.scale({ x: scale, y: scale });
const newPos = {
x: pointer.x - mousePointTo.x * scale,
y: pointer.y - mousePointTo.y * scale
};
stage.position(newPos);
stage.batchDraw();
});
// Submit code or file for parsing
function submitCode() {
const fileInput = document.getElementById('codeFile');
const codeInput = document.getElementById('codeInput').value;
const formData = new FormData();
if (fileInput.files.length > 0) {
formData.append('file', fileInput.files[0]);
} else if (codeInput) {
formData.append('code', codeInput);
} else {
alert('Please upload a file or paste code.');
return;
}
fetch('/parse_code', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
alert(data.error);
return;
}
clearCanvas();
parsedConnections = data.connections;
createNodesFromParsedData(data.nodes, data.connections);
})
.catch(error => console.error('Error:', error));
}
// Clear existing nodes and connections
function clearCanvas() {
nodes.forEach(node => node.destroy());
layer.find('Shape').forEach(shape => shape.destroy());
nodes = [];
connections = [];
parsedConnections = [];
if (codeWindow) {
codeWindow.destroy();
codeWindow = null;
}
if (codeTextarea) {
codeTextarea.remove();
codeTextarea = null;
}
layer.draw();
}
// Create nodes and connections from parsed data
function createNodesFromParsedData(parsedNodes, parsedConnections) {
const columns = {
imports: { x: 50, y: 50, count: 0 }, // Column for imports
global: { x: 250, y: 50, count: 0 }, // Column for global scope
functions: {} // Columns for functions
};
// First pass: Assign function nodes to columns
parsedNodes.forEach(nodeData => {
if (nodeData.type === 'function') {
const functionId = nodeData.id || `Function_${Object.keys(columns.functions).length + 1}`;
columns.functions[functionId] = {
x: 450 + Object.keys(columns.functions).length * 200,
y: 50,
count: 0
};
}
});
// Second pass: Create nodes with column-based positioning
parsedNodes.forEach(nodeData => {
const parentPath = nodeData.parent_path || 'global';
const level = nodeData.level || 0;
let x, y;
if (nodeData.type === 'import') {
// Imports column
x = columns.imports.x;
y = columns.imports.y + columns.imports.count * 80;
columns.imports.count++;
} else if (nodeData.type === 'function') {
// Function column
const functionId = nodeData.id;
x = columns.functions[functionId].x;
y = columns.functions[functionId].y + columns.functions[functionId].count * 80;
columns.functions[functionId].count++;
} else if (parentPath !== 'global' && parentPath.includes('Function')) {
// Child of a function
const functionId = parentPath.split(' -> ')[0];
if (columns.functions[functionId]) {
x = columns.functions[functionId].x;
y = columns.functions[functionId].y + columns.functions[functionId].count * 80;
columns.functions[functionId].count++;
} else {
// Fallback to global if function not found
x = columns.global.x;
y = columns.global.y + columns.global.count * 80;
columns.global.count++;
}
} else {
// Global scope (non-import, non-function)
x = columns.global.x;
y = columns.global.y + columns.global.count * 80;
columns.global.count++;
}
const node = createNode(
x,
y,
nodeData.label || 'Unnamed',
nodeData.type || 'other',
nodeData.inputs || [],
nodeData.outputs || [],
nodeData.id || `node_${nodes.length}`,
nodeData.source || '',
parentPath,
level
);
nodes.push(node);
layer.add(node);
});
layer.draw();
autoConnect();
saveNodes();
}
// Create a node with inputs, outputs, and code segment
function createNode(x, y, label, type, inputs = [], outputs = [], id, source = '', parent_path = 'global', level = 0) {
const node = new Konva.Group({
x: x,
y: y,
draggable: true
});
// Node rectangle
const isNumberBox = type === 'number_box';
const color = isNumberBox ? '#ffcccb' : type === 'function' ? '#ffeb3b' : type.includes('variable') ? '#90caf9' : type === 'import' ? '#a5d6a7' : '#ccc';
const width = isNumberBox ? 80 : 100;
const height = isNumberBox ? 40 : 50;
const box = new Konva.Rect({
width: width,
height: height,
fill: color,
stroke: 'black',
strokeWidth: 2,
cornerRadius: 5
});
// Node label
const text = new Konva.Text({
text: label,
fontSize: 12,
fontFamily: 'Arial',
fill: 'black',
width: width,
align: 'center',
y: isNumberBox ? 14 : 20
});
node.add(box);
node.add(text);
// Input/output ports
const inputPorts = inputs.map((input, i) => ({
id: `input-${id}-${i}`,
name: input,
circle: new Konva.Circle({
x: 0,
y: 10 + i * 20,
radius: 5,
fill: 'red'
})
}));
const outputPorts = outputs.map((output, i) => ({
id: `output-${id}-${i}`,
name: output,
circle: new Konva.Circle({
x: width,
y: 10 + i * 20,
radius: 5,
fill: 'green'
})
}));
// Add ports to node and set up click handlers
inputPorts.forEach(port => {
node.add(port.circle);
port.circle.on('click', () => {
if (!selectedPort) {
selectedPort = { node, portId: port.id, type: 'input' };
disconnectMode = true;
} else if (selectedPort.type === 'output' && selectedPort.node !== node) {
createSplineConnection(
selectedPort.node,
selectedPort.portId,
node,
port.id
);
connections.push({
fromNodeId: selectedPort.node.data.id,
fromPortId: selectedPort.portId,
toNodeId: node.data.id,
toPortId: port.id
});
selectedPort = null;
disconnectMode = false;
saveNodes();
} else if (disconnectMode && selectedPort.type === 'input' && selectedPort.node === node) {
selectedPort = { node, portId: port.id, type: 'input' };
} else {
selectedPort = null;
disconnectMode = false;
}
});
});
outputPorts.forEach(port => {
node.add(port.circle);
port.circle.on('click', () => {
if (!selectedPort) {
selectedPort = { node, portId: port.id, type: 'output' };
disconnectMode = false;
} else if (disconnectMode && selectedPort.type === 'input') {
const connIndex = connections.findIndex(
c => c.toNodeId === selectedPort.node.data.id &&
c.toPortId === selectedPort.portId &&
c.fromNodeId === node.data.id &&
c.fromPortId === port.id
);
if (connIndex !== -1) {
const conn = connections[connIndex];
const spline = layer.find('Shape').find(s =>
s.data.fromNodeId === conn.fromNodeId &&
s.data.fromPortId === conn.fromPortId &&
s.data.toNodeId === conn.toNodeId &&
s.data.toPortId === conn.toPortId
);
if (spline) spline.destroy();
connections.splice(connIndex, 1);
layer.draw();
saveNodes();
}
selectedPort = null;
disconnectMode = false;
} else {
selectedPort = null;
disconnectMode = false;
}
});
});
// Node data
node.data = {
id: id,
type: type,
label: label,
inputs: inputPorts,
outputs: outputPorts,
x: x,
y: y,
source: source,
parent_path: parent_path,
level: level
};
// Click handler to show code window
box.on('click', () => {
if (codeWindow) {
codeWindow.destroy();
codeWindow = null;
}
if (codeTextarea) {
codeTextarea.remove();
codeTextarea = null;
}
const nodePos = node.getAbsolutePosition();
// Create textarea for editing
codeTextarea = document.createElement('textarea');
codeTextarea.style.position = 'relative';
const canvasRect = stage.container().getBoundingClientRect();
const textareaX = nodePos.x;
// const textareaX = (nodePos.x + stage.x()) / scale + canvasRect.left;
// const textareaY = (nodePos.y + height + 10 + stage.y()) / scale + canvasRect.top;
const textareaY = nodePos.y;
//codeTextarea.style.right = `${textareaX}px`;
//codeTextarea.style.top = `${textareaY}px`;
codeTextarea.style.left = '15px' || `${textareaX}px`;
codeTextarea.style.top = '-200px' || `${textareaY}px`;
codeTextarea.style.width = '300px' || `${300 / scale}px`;
codeTextarea.style.height = '200px' || `${100 / scale}px`;
codeTextarea.style.fontFamily = 'monospace';
codeTextarea.style.fontSize = '12px' || `${12 / scale}px`;
codeTextarea.style.background = '#ebebeb';
codeTextarea.style.border = 'solid';
codeTextarea.style.borderColor = 'grey';
codeTextarea.style.resize = 'none';
codeTextarea.value = source || '';
document.body.appendChild(codeTextarea);
codeWindow = new Konva.Group({
x: node.x(),
y: node.y() + height + 10
});
const codeBox = new Konva.Rect({
width: 300,
height: 100,
fill: '#f0f0f0',
stroke: 'black',
strokeWidth: 1,
cornerRadius: 5
});
// Display source in Konva Text for visual containment
const codeText = new Konva.Text({
x: 5,
y: 5,
text: source || '',
fontSize: 12,
fontFamily: 'monospace',
fill: 'black',
width: 290,
padding: 5
});
//codeWindow.add(codeBox);
//codeWindow.add(codeText);
codeWindow.add(codeBox);
codeWindow.add(codeText);
//codeWindow.add(codeTextarea);
layer.add(codeWindow);
// Update code on change
codeTextarea.addEventListener('change', () => {
const newSource = codeTextarea.value;
node.data.source = newSource;
codeText.text(newSource);
fetch('/update_node', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: node.data.id,
source: newSource
})
})
.then(response => response.json())
.then(data => {
if (data.error) {
alert(data.error);
} else {
console.log('Node updated:', data);
updateProgram();
}
})
.catch(error => console.error('Error:', error));
});
// Close window on click outside
stage.on('click', (e) => {
if (e.target !== box && codeWindow) {
codeWindow.destroy();
codeWindow = null;
if (codeTextarea) {
codeTextarea.remove();
codeTextarea = null;
}
stage.off('click');
}
});
layer.draw();
});
// Update position and connections on drag
node.on('dragmove', () => {
node.data.x = node.x();
node.data.y = node.y();
if (codeWindow && codeTextarea) {
const nodePos = node.getAbsolutePosition();
codeWindow.position({ x: node.x(), y: node.y() + height + 10 });
const canvasRect = stage.container().getBoundingClientRect();
const textareaX = (nodePos.x + stage.x()) / scale + canvasRect.left;
const textareaY = (nodePos.y + height + 10 + stage.y()) / scale + canvasRect.top;
codeTextarea.style.left = `${textareaX}px`;
codeTextarea.style.top = `${textareaY}px`;
}
updateConnections();
saveNodes();
});
return node;
}
// Create a spline connection
function createSplineConnection(fromNode, fromPortId, toNode, toPortId) {
const fromPort = fromNode.data.outputs.find(p => p.id === fromPortId);
const toPort = toNode.data.inputs.find(p => p.id === toPortId);
if (!fromPort || !toPort) return;
const startX = fromNode.x() + fromPort.circle.x();
const startY = fromNode.y() + fromPort.circle.y();
const endX = toNode.x() + toPort.circle.x();
const endY = toNode.y() + toPort.circle.y();
const control1X = startX + (endX - startX) / 3;
const control1Y = startY;
const control2X = startX + 2 * (endX - startX) / 3;
const control2Y = endY;
const spline = new Konva.Shape({
sceneFunc: function(context, shape) {
context.beginPath();
context.moveTo(startX, startY);
context.bezierCurveTo(control1X, control1Y, control2X, control2Y, endX, endY);
context.fillStrokeShape(shape);
},
stroke: 'black',
strokeWidth: 2
});
spline.data = {
fromNodeId: fromNode.data.id,
fromPortId: fromPortId,
toNodeId: toNode.data.id,
toPortId: toPortId
};
layer.add(spline);
layer.draw();
}
// Enhanced auto-connect based on hierarchy, position, and role
function autoConnect() {
layer.find('Shape').forEach(shape => {
if (shape.data && shape.data.fromNodeId !== undefined) {
shape.destroy();
}
});
connections = [];
const sortedNodes = [...nodes].sort((a, b) => {
if (a.data.level !== b.data.level) return a.data.level - b.data.level;
return a.data.y - b.data.y; // Sort by y within columns
});
const hierarchy = {};
sortedNodes.forEach(node => {
const parent = node.data.parent_path.split(' -> ')[0] || 'global';
if (!hierarchy[parent]) hierarchy[parent] = [];
hierarchy[parent].push(node);
});
parsedConnections.forEach(conn => {
const fromNode = nodes.find(n => n.data.id === conn.from);
const toNode = nodes.find(n => n.data.id === conn.to);
if (fromNode && toNode) {
const fromPort = fromNode.data.outputs[0];
const toPort = toNode.data.inputs[0];
if (fromPort && toPort) {
createSplineConnection(fromNode, fromPort.id, toNode, toPort.id);
connections.push({
fromNodeId: fromNode.data.id,
fromPortId: fromPort.id,
toNodeId: toNode.data.id,
toPortId: toPort.id
});
}
}
});
sortedNodes.forEach(node => {
const nodeId = node.data.id;
const parent = node.data.parent_path.split(' -> ')[0] || 'global';
const role = node.data.type;
if (parent !== 'global') {
const parentNode = nodes.find(n => n.data.id === parent || n.data.label === parent.split('[')[0]);
if (parentNode && parentNode.data.outputs.length > 0 && node.data.inputs.length > 0) {
const fromPort = parentNode.data.outputs[0];
const toPort = node.data.inputs[0];
if (!connections.some(c => c.fromNodeId === parentNode.data.id && c.toNodeId === nodeId)) {
createSplineConnection(parentNode, fromPort.id, node, toPort.id);
connections.push({
fromNodeId: parentNode.data.id,
fromPortId: fromPort.id,
toNodeId: nodeId,
toPortId: toPort.id
});
}
}
}
if (role.includes('variable')) {
const varName = node.data.label;
sortedNodes.forEach(otherNode => {
if (otherNode !== node && otherNode.data.source.includes(varName)) {
const fromPort = node.data.outputs[0];
const toPort = otherNode.data.inputs[0];
if (fromPort && toPort && !connections.some(c => c.fromNodeId === nodeId && c.toNodeId === otherNode.data.id)) {
createSplineConnection(node, fromPort.id, otherNode, toPort.id);
connections.push({
fromNodeId: nodeId,
fromPortId: fromPort.id,
toNodeId: otherNode.data.id,
toPortId: toPort.id
});
}
}
});
}
});
layer.draw();
saveNodes();
}
// Update full program
function updateProgram() {
const program = reconstructProgram();
document.getElementById('codeInput').value = program;
fetch('/update_program', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: program })
})
.then(response => response.json())
.then(data => {
if (data.error) {
alert(data.error);
} else {
console.log('Program updated:', data);
}
})
.catch(error => console.error('Error:', error));
}
// Reconstruct the original program
function reconstructProgram() {
const sortedNodes = [...nodes].sort((a, b) => {
if (a.data.level !== b.data.level) return a.data.level - b.data.level;
return a.data.y - b.data.y; // Sort by y within columns
});
let program = '';
sortedNodes.forEach(node => {
const source = node.data.source || '';
const level = node.data.level || 0;
const indent = ' '.repeat(level);
program += indent + source.trim() + '\n';
});
return program.trim();
}
// Add a manual node
function addNode() {
const node = createNode(
250, // Add to global column
50 + nodes.filter(n => n.data.parent_path === 'global').length * 80,
'Function',
'function',
['in1'],
['out1'],
nodes.length,
'def new_function(): pass',
'global',
0
);
nodes.push(node);
layer.add(node);
layer.draw();
saveNodes();
}
// Update spline connections when nodes move
function createSplineConnection(fromNode, fromPortId, toNode, toPortId) {
const fromPort = fromNode.data.outputs.find(p => p.id === fromPortId);
const toPort = toNode.data.inputs.find(p => p.id === toPortId);
if (!fromPort || !toPort) return;
const startX = fromNode.x() + fromPort.circle.x();
const startY = fromNode.y() + fromPort.circle.y();
const endX = toNode.x() + toPort.circle.x();
const endY = toNode.y() + toPort.circle.y();
const control1X = startX + (endX - startX) / 3;
const control1Y = startY;
const control2X = startX + 2 * (endX - startX) / 3;
const control2Y = endY;
const spline = new Konva.Shape({
sceneFunc: function(context, shape) {
context.beginPath();
context.moveTo(startX, startY);
context.bezierCurveTo(control1X, control1Y, control2X, control2Y, endX, endY);
context.fillStrokeShape(shape);
},
stroke: 'black',
strokeWidth: 2
});
spline.data = {
fromNodeId: fromNode.data.id,
fromPortId: fromPortId,
toNodeId: toNode.data.id,
toPortId: toPortId
};
layer.add(spline);
layer.draw();
}
// Update spline connections when nodes move
function updateConnections() {
layer.find('Shape').forEach(shape => {
if (shape.data && shape.data.fromNodeId !== undefined) {
const fromNode = nodes.find(n => n.data.id === shape.data.fromNodeId);
const toNode = nodes.find(n => n.data.id === shape.data.toNodeId);
if (fromNode && toNode) {
const fromPort = fromNode.data.outputs.find(p => p.id === shape.data.fromPortId);
const toPort = toNode.data.inputs.find(p => p.id === shape.data.toPortId);
if (fromPort && toPort) {
const startX = fromNode.x() + fromPort.circle.x();
const startY = fromNode.y() + fromPort.circle.y();
const endX = toNode.x() + toPort.circle.x();
const endY = toNode.y() + toPort.circle.y();
const control1X = startX + (endX - startX) / 3;
const control1Y = startY;
const control2X = startX + 2 * (endX - startX) / 3;
const control2Y = endY;
shape.sceneFunc(function(context, shape) {
context.beginPath();
context.moveTo(startX, startY);
context.bezierCurveTo(control1X, control1Y, control2X, control2Y, endX, endY);
context.fillStrokeShape(shape);
});
}
}
}
});
layer.draw();
}
// Save nodes and connections to backend
function saveNodes() {
fetch('/save_nodes', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
nodes: nodes.map(n => ({
id: n.data.id,
type: n.data.type,
label: n.data.label,
x: n.data.x,
y: n.data.y,
inputs: n.data.inputs.map(p => p.name),
outputs: n.data.outputs.map(p => p.name),
source: n.data.source,
parent_path: n.data.parent_path,
level: n.data.level
})),
connections: connections
})
})
.then(response => response.json())
.then(data => console.log('Saved:', data))
.catch(error => console.error('Error:', error));
}
// Initial draw
layer.draw();