noding / static /canvas.js
broadfield-dev's picture
Update static/canvas.js
d615ba9 verified
raw
history blame
11.9 kB
// Initialize Konva stage
const stage = new Konva.Stage({
container: 'container',
width: 1000,
height: 600
});
const layer = new Konva.Layer();
stage.add(layer);
// Store nodes, connections, and parsed data
let nodes = [];
let connections = []; // { fromNodeId, fromPortId, toNodeId, toPortId }
let parsedConnections = [];
let selectedPort = null; // { node, portId, type: 'input'|'output' }
let disconnectMode = false;
// 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; // Store for auto-connect
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 = [];
layer.draw();
}
// Create nodes and connections from parsed data
function createNodesFromParsedData(parsedNodes, parsedConnections) {
parsedNodes.forEach(nodeData => {
const node = createNode(
nodeData.x,
nodeData.y,
nodeData.label,
nodeData.type,
nodeData.inputs,
nodeData.outputs,
nodeData.id
);
nodes.push(node);
layer.add(node);
});
layer.draw();
saveNodes();
}
// Create a node with inputs and outputs
function createNode(x, y, label, type, inputs = [], outputs = [], id) {
const node = new Konva.Group({
x: x,
y: y,
draggable: true
});
// Node rectangle
const color = type === 'function' ? '#ffeb3b' : type.includes('variable') ? '#90caf9' : type === 'import' ? '#a5d6a7' : '#ccc';
const box = new Konva.Rect({
width: 100,
height: 50,
fill: color,
stroke: 'black',
strokeWidth: 2,
cornerRadius: 5
});
// Node label
const text = new Konva.Text({
text: label,
fontSize: 12,
fontFamily: 'Arial',
fill: 'black',
width: 100,
align: 'center',
y: 20
});
node.add(box);
node.add(text);
// Add input/output ports with unique IDs
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: 100,
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; // First click on input enables disconnect mode
} else if (selectedPort.type === 'output' && selectedPort.node !== node) {
// Connect output to input
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) {
// Select output to disconnect
selectedPort = { node, portId: port.id, type: 'input' }; // Keep input selected
} 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') {
// Disconnect input from output
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
};
// Update position and connections on drag
node.on('dragmove', () => {
node.data.x = node.x();
node.data.y = node.y();
updateConnections();
saveNodes();
});
return node;
}
// Create a spline (Bezier curve) 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();
// Control points for Bezier curve
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();
}
// Auto-connect nodes based on parsed connections
function autoConnect() {
// Clear existing connections
layer.find('Shape').forEach(shape => {
if (shape.data && shape.data.fromNodeId !== undefined) {
shape.destroy();
}
});
connections = [];
// Create spline connections for parsed data
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) {
// Find first available output and input ports
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
});
}
}
});
layer.draw();
saveNodes();
}
// Add a manual node
function addNode() {
const node = createNode(
Math.random() * (stage.width() - 100),
Math.random() * (stage.height() - 100),
'Function',
'function',
['in1'],
['out1'],
nodes.length
);
nodes.push(node);
layer.add(node);
layer.draw();
saveNodes();
}
// 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)
})),
connections: connections
})
}).then(response => response.json())
.then(data => console.log('Saved:', data))
.catch(error => console.error('Error:', error));
}
// Initial draw
layer.draw();