|
|
|
function initializeDragAndDrop() { |
|
const nodeItems = document.querySelectorAll('.node-item'); |
|
const canvas = document.getElementById('network-canvas'); |
|
let draggedNode = null; |
|
let offsetX, offsetY; |
|
let isDragging = false; |
|
let isConnecting = false; |
|
let startNode = null; |
|
let connectionLine = null; |
|
let nodeCounter = {}; |
|
|
|
|
|
let networkLayers = { |
|
layers: [], |
|
connections: [] |
|
}; |
|
|
|
|
|
nodeItems.forEach(item => { |
|
item.addEventListener('dragstart', handleDragStart); |
|
}); |
|
|
|
|
|
canvas.addEventListener('dragover', handleDragOver); |
|
canvas.addEventListener('drop', handleDrop); |
|
|
|
|
|
function handleDragStart(e) { |
|
draggedNode = this; |
|
e.dataTransfer.setData('text/plain', this.getAttribute('data-type')); |
|
|
|
|
|
const ghost = this.cloneNode(true); |
|
ghost.style.opacity = '0.5'; |
|
document.body.appendChild(ghost); |
|
e.dataTransfer.setDragImage(ghost, 0, 0); |
|
setTimeout(() => { |
|
document.body.removeChild(ghost); |
|
}, 0); |
|
} |
|
|
|
|
|
function handleDragOver(e) { |
|
e.preventDefault(); |
|
e.dataTransfer.dropEffect = 'copy'; |
|
} |
|
|
|
|
|
function handleDrop(e) { |
|
e.preventDefault(); |
|
|
|
|
|
const canvasHint = document.querySelector('.canvas-hint'); |
|
if (canvasHint) { |
|
canvasHint.style.display = 'none'; |
|
} |
|
|
|
const nodeType = e.dataTransfer.getData('text/plain'); |
|
|
|
if (nodeType) { |
|
|
|
const layerId = window.neuralNetwork.getNextLayerId(nodeType); |
|
|
|
|
|
const canvasNode = document.createElement('div'); |
|
canvasNode.className = `canvas-node ${nodeType}-node`; |
|
canvasNode.setAttribute('data-type', nodeType); |
|
canvasNode.setAttribute('data-id', layerId); |
|
|
|
|
|
const rect = canvas.getBoundingClientRect(); |
|
const x = e.clientX - rect.left; |
|
const y = e.clientY - rect.top; |
|
|
|
canvasNode.style.left = `${x}px`; |
|
canvasNode.style.top = `${y}px`; |
|
|
|
|
|
let nodeName, dimensions, units; |
|
|
|
switch(nodeType) { |
|
case 'input': |
|
nodeName = 'Input Layer'; |
|
dimensions = '1 × 28 × 28'; |
|
break; |
|
case 'hidden': |
|
|
|
const hiddenCount = document.querySelectorAll('.canvas-node[data-type="hidden"]').length; |
|
units = hiddenCount === 0 ? 128 : 64; |
|
nodeName = `Hidden Layer ${hiddenCount + 1}`; |
|
dimensions = `${units}`; |
|
break; |
|
case 'output': |
|
nodeName = 'Output Layer'; |
|
dimensions = '10'; |
|
break; |
|
case 'conv': |
|
const convCount = document.querySelectorAll('.canvas-node[data-type="conv"]').length; |
|
const filters = 32 * (convCount + 1); |
|
nodeName = `Conv2D ${convCount + 1}`; |
|
dimensions = `${filters} × 26 × 26`; |
|
break; |
|
case 'pool': |
|
const poolCount = document.querySelectorAll('.canvas-node[data-type="pool"]').length; |
|
nodeName = `MaxPool ${poolCount + 1}`; |
|
dimensions = '32 × 13 × 13'; |
|
break; |
|
default: |
|
nodeName = 'Neural Node'; |
|
dimensions = '64'; |
|
} |
|
|
|
canvasNode.innerHTML = ` |
|
<div class="node-title">${nodeName}</div> |
|
<div class="node-id">${layerId}</div> |
|
<div class="node-dimensions">${dimensions}</div> |
|
<div class="node-port port-in"></div> |
|
<div class="node-port port-out"></div> |
|
<div class="node-controls"> |
|
<button class="node-edit-btn" title="Edit layer parameters"><i class="icon">⚙️</i></button> |
|
<button class="node-delete-btn" title="Delete layer"><i class="icon">🗑️</i></button> |
|
</div> |
|
`; |
|
|
|
|
|
canvasNode.setAttribute('data-dimensions', dimensions); |
|
canvasNode.setAttribute('data-name', nodeName); |
|
|
|
|
|
const layerInfo = { |
|
id: layerId, |
|
type: nodeType, |
|
name: nodeName, |
|
dimensions: dimensions, |
|
position: { x, y } |
|
}; |
|
|
|
networkLayers.layers.push(layerInfo); |
|
|
|
|
|
canvas.appendChild(canvasNode); |
|
|
|
|
|
canvasNode.addEventListener('mousedown', startDrag); |
|
|
|
|
|
const portIn = canvasNode.querySelector('.port-in'); |
|
const portOut = canvasNode.querySelector('.port-out'); |
|
|
|
portOut.addEventListener('mousedown', (e) => { |
|
e.stopPropagation(); |
|
startConnection(canvasNode, e); |
|
}); |
|
|
|
portIn.addEventListener('mouseup', (e) => { |
|
e.stopPropagation(); |
|
endConnection(canvasNode); |
|
}); |
|
|
|
|
|
const editBtn = canvasNode.querySelector('.node-edit-btn'); |
|
if (editBtn) { |
|
editBtn.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
openLayerEditor(canvasNode); |
|
}); |
|
} |
|
|
|
const deleteBtn = canvasNode.querySelector('.node-delete-btn'); |
|
if (deleteBtn) { |
|
deleteBtn.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
deleteNode(canvasNode); |
|
}); |
|
} |
|
|
|
|
|
updateLayerConnectivity(); |
|
} |
|
} |
|
|
|
|
|
function startDrag(e) { |
|
if (isConnecting) return; |
|
|
|
|
|
if (e.target.closest('.node-controls') || e.target.closest('.node-port')) { |
|
return; |
|
} |
|
|
|
isDragging = true; |
|
const target = e.target.closest('.canvas-node'); |
|
const rect = target.getBoundingClientRect(); |
|
|
|
|
|
offsetX = e.clientX - rect.left; |
|
offsetY = e.clientY - rect.top; |
|
|
|
document.addEventListener('mousemove', dragNode); |
|
document.addEventListener('mouseup', stopDrag); |
|
|
|
|
|
draggedNode = target; |
|
|
|
|
|
draggedNode.style.zIndex = "100"; |
|
|
|
|
|
draggedNode.classList.add('dragging'); |
|
|
|
|
|
e.preventDefault(); |
|
} |
|
|
|
|
|
function dragNode(e) { |
|
if (!isDragging) return; |
|
|
|
const canvasRect = canvas.getBoundingClientRect(); |
|
let x = e.clientX - canvasRect.left - offsetX; |
|
let y = e.clientY - canvasRect.top - offsetY; |
|
|
|
|
|
x = Math.max(0, Math.min(canvasRect.width - draggedNode.offsetWidth, x)); |
|
y = Math.max(0, Math.min(canvasRect.height - draggedNode.offsetHeight, y)); |
|
|
|
draggedNode.style.left = `${x}px`; |
|
draggedNode.style.top = `${y}px`; |
|
|
|
|
|
const nodeId = draggedNode.getAttribute('data-id'); |
|
const layerIndex = networkLayers.layers.findIndex(layer => layer.id === nodeId); |
|
if (layerIndex !== -1) { |
|
networkLayers.layers[layerIndex].position = { x, y }; |
|
} |
|
|
|
|
|
updateConnections(); |
|
} |
|
|
|
|
|
function stopDrag() { |
|
if (!isDragging) return; |
|
|
|
isDragging = false; |
|
document.removeEventListener('mousemove', dragNode); |
|
document.removeEventListener('mouseup', stopDrag); |
|
|
|
|
|
if (draggedNode) { |
|
draggedNode.style.zIndex = "10"; |
|
draggedNode.classList.remove('dragging'); |
|
|
|
|
|
updateConnections(); |
|
} |
|
} |
|
|
|
|
|
function startConnection(node, e) { |
|
isConnecting = true; |
|
startNode = node; |
|
|
|
|
|
connectionLine = document.createElement('div'); |
|
connectionLine.className = 'connection temp-connection'; |
|
|
|
|
|
const portOut = node.querySelector('.port-out'); |
|
const portRect = portOut.getBoundingClientRect(); |
|
const canvasRect = canvas.getBoundingClientRect(); |
|
|
|
const startX = portRect.left + portRect.width / 2 - canvasRect.left; |
|
const startY = portRect.top + portRect.height / 2 - canvasRect.top; |
|
|
|
|
|
connectionLine.style.left = `${startX}px`; |
|
connectionLine.style.top = `${startY}px`; |
|
connectionLine.style.width = '0px'; |
|
connectionLine.style.transform = 'rotate(0deg)'; |
|
|
|
|
|
portOut.classList.add('active-port'); |
|
|
|
|
|
highlightValidConnectionTargets(node); |
|
|
|
canvas.appendChild(connectionLine); |
|
|
|
|
|
document.addEventListener('mousemove', drawConnection); |
|
document.addEventListener('mouseup', cancelConnection); |
|
|
|
e.preventDefault(); |
|
} |
|
|
|
|
|
function highlightValidConnectionTargets(sourceNode) { |
|
const sourceType = sourceNode.getAttribute('data-type'); |
|
const sourceId = sourceNode.getAttribute('data-id'); |
|
|
|
document.querySelectorAll('.canvas-node').forEach(node => { |
|
if (node !== sourceNode) { |
|
const nodeType = node.getAttribute('data-type'); |
|
const nodeId = node.getAttribute('data-id'); |
|
const isValidTarget = isValidConnection(sourceType, nodeType, sourceId, nodeId); |
|
|
|
const portIn = node.querySelector('.port-in'); |
|
if (isValidTarget) { |
|
portIn.classList.add('valid-target'); |
|
} else { |
|
portIn.classList.add('invalid-target'); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
function removePortHighlights() { |
|
document.querySelectorAll('.port-in, .port-out').forEach(port => { |
|
port.classList.remove('active-port', 'valid-target', 'invalid-target'); |
|
}); |
|
} |
|
|
|
|
|
function isValidConnection(sourceType, targetType, sourceId, targetId) { |
|
|
|
if (sourceType === 'output' || targetType === 'input') { |
|
return false; |
|
} |
|
|
|
|
|
const existingConnection = networkLayers.connections.find( |
|
conn => conn.target === sourceId && conn.source === targetId |
|
); |
|
if (existingConnection) { |
|
return false; |
|
} |
|
|
|
|
|
switch(sourceType) { |
|
case 'input': |
|
return ['hidden', 'conv'].includes(targetType); |
|
case 'conv': |
|
return ['conv', 'pool', 'hidden'].includes(targetType); |
|
case 'pool': |
|
return ['conv', 'hidden'].includes(targetType); |
|
case 'hidden': |
|
return ['hidden', 'output'].includes(targetType); |
|
default: |
|
return false; |
|
} |
|
} |
|
|
|
|
|
function drawConnection(e) { |
|
if (!isConnecting || !connectionLine) return; |
|
|
|
const canvasRect = canvas.getBoundingClientRect(); |
|
const portOut = startNode.querySelector('.port-out'); |
|
const portRect = portOut.getBoundingClientRect(); |
|
|
|
|
|
const startX = portRect.left + portRect.width / 2 - canvasRect.left; |
|
const startY = portRect.top + portRect.height / 2 - canvasRect.top; |
|
const endX = e.clientX - canvasRect.left; |
|
const endY = e.clientY - canvasRect.top; |
|
|
|
|
|
const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); |
|
const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI; |
|
|
|
|
|
connectionLine.style.width = `${length}px`; |
|
connectionLine.style.transform = `rotate(${angle}deg)`; |
|
|
|
|
|
document.querySelectorAll('.canvas-node').forEach(node => { |
|
if (node !== startNode) { |
|
const nodeRect = node.getBoundingClientRect(); |
|
const portIn = node.querySelector('.port-in'); |
|
const portInRect = portIn.getBoundingClientRect(); |
|
|
|
|
|
if (e.clientX >= portInRect.left && e.clientX <= portInRect.right && |
|
e.clientY >= portInRect.top && e.clientY <= portInRect.bottom) { |
|
portIn.classList.add('port-hover'); |
|
} else { |
|
portIn.classList.remove('port-hover'); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
function cancelConnection(e) { |
|
if (!isConnecting) return; |
|
|
|
|
|
let targetNode = null; |
|
document.querySelectorAll('.canvas-node').forEach(node => { |
|
if (node !== startNode) { |
|
const portIn = node.querySelector('.port-in'); |
|
const portRect = portIn.getBoundingClientRect(); |
|
|
|
if (e.clientX >= portRect.left && e.clientX <= portRect.right && |
|
e.clientY >= portRect.top && e.clientY <= portRect.bottom) { |
|
|
|
|
|
const sourceType = startNode.getAttribute('data-type'); |
|
const targetType = node.getAttribute('data-type'); |
|
const sourceId = startNode.getAttribute('data-id'); |
|
const targetId = node.getAttribute('data-id'); |
|
|
|
if (isValidConnection(sourceType, targetType, sourceId, targetId)) { |
|
targetNode = node; |
|
} |
|
} |
|
} |
|
}); |
|
|
|
|
|
if (targetNode) { |
|
endConnection(targetNode); |
|
} else { |
|
|
|
if (connectionLine && connectionLine.parentNode) { |
|
connectionLine.parentNode.removeChild(connectionLine); |
|
} |
|
} |
|
|
|
|
|
removePortHighlights(); |
|
document.querySelectorAll('.port-hover').forEach(port => { |
|
port.classList.remove('port-hover'); |
|
}); |
|
|
|
|
|
isConnecting = false; |
|
startNode = null; |
|
connectionLine = null; |
|
|
|
|
|
document.removeEventListener('mousemove', drawConnection); |
|
document.removeEventListener('mouseup', cancelConnection); |
|
} |
|
|
|
|
|
function endConnection(targetNode) { |
|
if (!isConnecting) return; |
|
|
|
|
|
if (targetNode && targetNode.classList && targetNode.classList.contains('canvas-node')) { |
|
|
|
const sourceId = startNode.getAttribute('data-id'); |
|
const targetId = targetNode.getAttribute('data-id'); |
|
|
|
|
|
const exists = networkLayers.connections.some(conn => |
|
conn.source === sourceId && conn.target === targetId |
|
); |
|
|
|
if (!exists) { |
|
|
|
const connection = connectionLine.cloneNode(true); |
|
connection.classList.remove('temp-connection'); |
|
connection.setAttribute('data-source', sourceId); |
|
connection.setAttribute('data-target', targetId); |
|
canvas.appendChild(connection); |
|
|
|
|
|
networkLayers.connections.push({ |
|
source: sourceId, |
|
target: targetId, |
|
sourceType: startNode.getAttribute('data-type'), |
|
targetType: targetNode.getAttribute('data-type') |
|
}); |
|
|
|
|
|
updateLayerConnectivity(); |
|
|
|
console.log(`Connected ${sourceId} to ${targetId}`); |
|
} |
|
} |
|
|
|
|
|
if (connectionLine && connectionLine.parentNode) { |
|
connectionLine.parentNode.removeChild(connectionLine); |
|
} |
|
|
|
|
|
removePortHighlights(); |
|
|
|
|
|
isConnecting = false; |
|
startNode = null; |
|
connectionLine = null; |
|
|
|
|
|
document.removeEventListener('mousemove', drawConnection); |
|
document.removeEventListener('mouseup', cancelConnection); |
|
} |
|
|
|
|
|
function updateLayerConnectivity() { |
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.canvas-node').forEach(node => { |
|
node.classList.remove('connected-node'); |
|
}); |
|
|
|
|
|
const connectedNodeIds = new Set(); |
|
networkLayers.connections.forEach(conn => { |
|
connectedNodeIds.add(conn.source); |
|
connectedNodeIds.add(conn.target); |
|
}); |
|
|
|
connectedNodeIds.forEach(id => { |
|
const node = document.querySelector(`.canvas-node[data-id="${id}"]`); |
|
if (node) { |
|
node.classList.add('connected-node'); |
|
} |
|
}); |
|
|
|
|
|
const event = new CustomEvent('networkUpdated', { detail: networkLayers }); |
|
document.dispatchEvent(event); |
|
} |
|
|
|
|
|
function deleteNode(node) { |
|
if (!node) return; |
|
|
|
const nodeId = node.getAttribute('data-id'); |
|
|
|
|
|
document.querySelectorAll(`.connection[data-source="${nodeId}"], .connection[data-target="${nodeId}"]`).forEach(conn => { |
|
conn.parentNode.removeChild(conn); |
|
}); |
|
|
|
|
|
networkLayers.layers = networkLayers.layers.filter(layer => layer.id !== nodeId); |
|
networkLayers.connections = networkLayers.connections.filter(conn => |
|
conn.source !== nodeId && conn.target !== nodeId |
|
); |
|
|
|
|
|
node.parentNode.removeChild(node); |
|
|
|
|
|
updateLayerConnectivity(); |
|
} |
|
|
|
|
|
function openLayerEditor(node) { |
|
if (!node) return; |
|
|
|
const nodeId = node.getAttribute('data-id'); |
|
const nodeType = node.getAttribute('data-type'); |
|
const nodeName = node.getAttribute('data-name'); |
|
const dimensions = node.getAttribute('data-dimensions'); |
|
|
|
|
|
const event = new CustomEvent('openLayerEditor', { |
|
detail: { id: nodeId, type: nodeType, name: nodeName, dimensions: dimensions } |
|
}); |
|
document.dispatchEvent(event); |
|
} |
|
|
|
|
|
function updateConnections() { |
|
const connections = document.querySelectorAll('.connection'); |
|
connections.forEach(connection => { |
|
const sourceId = connection.getAttribute('data-source'); |
|
const targetId = connection.getAttribute('data-target'); |
|
|
|
const sourceNode = document.querySelector(`.canvas-node[data-id="${sourceId}"]`); |
|
const targetNode = document.querySelector(`.canvas-node[data-id="${targetId}"]`); |
|
|
|
if (sourceNode && targetNode) { |
|
const sourcePort = sourceNode.querySelector('.port-out'); |
|
const targetPort = targetNode.querySelector('.port-in'); |
|
|
|
if (sourcePort && targetPort) { |
|
const sourceRect = sourcePort.getBoundingClientRect(); |
|
const targetRect = targetPort.getBoundingClientRect(); |
|
const canvasRect = canvas.getBoundingClientRect(); |
|
|
|
const startX = sourceRect.left + sourceRect.width / 2 - canvasRect.left; |
|
const startY = sourceRect.top + sourceRect.height / 2 - canvasRect.top; |
|
const endX = targetRect.left + targetRect.width / 2 - canvasRect.left; |
|
const endY = targetRect.top + targetRect.height / 2 - canvasRect.top; |
|
|
|
const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); |
|
const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI; |
|
|
|
connection.style.left = `${startX}px`; |
|
connection.style.top = `${startY}px`; |
|
connection.style.width = `${length}px`; |
|
connection.style.transform = `rotate(${angle}deg)`; |
|
} |
|
} else { |
|
|
|
if (connection.parentNode) { |
|
connection.parentNode.removeChild(connection); |
|
|
|
|
|
const connIndex = networkLayers.connections.findIndex(conn => |
|
conn.source === sourceId && conn.target === targetId |
|
); |
|
if (connIndex !== -1) { |
|
networkLayers.connections.splice(connIndex, 1); |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
function getNetworkArchitecture() { |
|
return networkLayers; |
|
} |
|
|
|
|
|
function clearAllNodes() { |
|
|
|
document.querySelectorAll('.canvas-node, .connection').forEach(el => { |
|
el.parentNode.removeChild(el); |
|
}); |
|
|
|
|
|
networkLayers = { |
|
layers: [], |
|
connections: [] |
|
}; |
|
|
|
|
|
window.neuralNetwork.resetLayerCounter(); |
|
|
|
|
|
const canvasHint = document.querySelector('.canvas-hint'); |
|
if (canvasHint) { |
|
canvasHint.style.display = 'block'; |
|
} |
|
|
|
|
|
const event = new CustomEvent('networkUpdated', { detail: networkLayers }); |
|
document.dispatchEvent(event); |
|
} |
|
|
|
|
|
window.dragDrop = { |
|
getNetworkArchitecture, |
|
clearAllNodes, |
|
updateConnections |
|
}; |
|
} |