Properties

Select a node to edit its properties

Ready - Drag circuits from the sidebar to create nodes. Click and drag between ports to create connections.
function createNode(circuitType, x, y) { console.log('=== CREATE NODE START ==='); console.log('Creating node:', circuitType, 'at', x, y); console.log('Available definitions:', Object.keys(circuitDefinitions)); const definition = circuitDefinitions[circuitType]; if (!definition) { console.error('No definition found for circuit type:', circuitType); alert('Error: No definition found for ' + circuitType); return; } const node = { id: ++nodeIdCounter, type: circuitType, x: x - viewOffset.x, y: y - viewOffset.y, inputs: {}, outputs: {}, definition: definition }; // Initialize input values Object.keys(definition.inputs).forEach(key => { node.inputs[key] = definition.inputs[key].default || '0'; }); nodes.push(node); console.log('Node created:', node); console.log('Total nodes now:', nodes.length); console.log('About to render...'); try { renderCanvas(); console.log('Render completed'); selectNode(node); console.log('Node selected'); } catch (error) { console.error('Error during rendering:', error); alert('Error during rendering: ' + error.message); } console.log('=== CREATE NODE END ==='); } function selectNode(node) { selectedNode = node; renderCanvas(); updatePropertiesPanel(); } function updatePropertiesPanel() { const panel = document.getElementById('properties-content'); if (!selectedNode) { panel.innerHTML = '

Select a node to edit its properties

'; return; } let html = `
Circuit Type
${selectedNode.definition.name}
`; // Input parameters if (Object.keys(selectedNode.definition.inputs).length > 0) { html += `
Input Parameters
`; Object.keys(selectedNode.definition.inputs).forEach(key => { const input = selectedNode.definition.inputs[key]; html += `
`; }); html += `
`; } panel.innerHTML = html; } function updateNodeInput(inputKey, value) { if (selectedNode) { selectedNode.inputs[inputKey] = value; renderCanvas(); } } function renderCanvas() { console.log('Rendering canvas with', nodes.length, 'nodes and', connections.length, 'connections'); // Render connections in SVG layer const svgLayer = document.getElementById('svg-layer'); let svgContent = ''; connections.forEach(conn => { const fromNode = nodes.find(n => n.id === conn.fromNodeId); const toNode = nodes.find(n => n.id === conn.toNodeId); if (fromNode && toNode) { const fromPos = getPortPosition(fromNode, conn.fromPort, 'output'); const toPos = getPortPosition(toNode, conn.toPort, 'input'); const curve = createConnectionCurve(fromPos, toPos); svgContent += ``; } }); svgLayer.innerHTML = svgContent; // Render nodes in nodes layer const nodesLayer = document.getElementById('nodes-layer'); let nodesContent = ''; nodes.forEach(node => { nodesContent += renderNode(node); }); nodesLayer.innerHTML = nodesContent; // Re-attach event listeners to new nodes attachNodeEventListeners(); } function attachNodeEventListeners() { // Attach event listeners to all nodes document.querySelectorAll('[data-node-id]').forEach(nodeEl => { const nodeId = parseInt(nodeEl.getAttribute('data-node-id')); // Node selection nodeEl.addEventListener('click', (e) => { const node = nodes.find(n => n.id === nodeId); if (node) { selectNode(node); } e.stopPropagation(); }); // Node dragging const header = nodeEl.querySelector('.node-header'); if (header) { header.addEventListener('mousedown', (e) => { draggedNode = nodes.find(n => n.id === nodeId); if (draggedNode) { selectNode(draggedNode); const rect = nodeEl.getBoundingClientRect(); draggedNode.dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; } e.stopPropagation(); }); } // Port event listeners nodeEl.querySelectorAll('.port').forEach(port => { port.addEventListener('mousedown', (e) => { const portKey = port.getAttribute('data-port'); const portType = port.getAttribute('data-port-type'); if (portType === 'output') { connectionStart = { nodeId, portKey, portType }; canvas.style.cursor = 'crosshair'; } e.stopPropagation(); }); port.addEventListener('mouseup', (e) => { if (connectionStart) { const toPortKey = port.getAttribute('data-port'); const toPortType = port.getAttribute('data-port-type'); if (toPortType === 'input' && nodeId !== connectionStart.nodeId) { createConnection(connectionStart.nodeId, connectionStart.portKey, nodeId, toPortKey); } } e.stopPropagation(); }); }); // Parameter input listeners nodeEl.querySelectorAll('.parameter-value').forEach(input => { input.addEventListener('change', (e) => { const node = nodes.find(n => n.id === nodeId); if (node) { // Find which parameter this input belongs to const parameter = e.target.closest('.parameter'); const paramName = parameter.querySelector('.parameter-name').textContent; // Find the actual parameter key const paramKey = Object.keys(node.definition.inputs).find(key => node.definition.inputs[key].name === paramName ); if (paramKey) { node.inputs[paramKey] = e.target.value; } } }); }); }); } function createConnection(fromNodeId, fromPort, toNodeId, toPort) { const connection = { fromNodeId, fromPort, toNodeId, toPort, id: Date.now() }; // Remove existing connection to this input connections = connections.filter(c => !(c.toNodeId === toNodeId && c.toPort === toPort) ); connections.push(connection); // Update the target node's input value to reference the connection const targetNode = nodes.find(n => n.id === toNodeId); if (targetNode) { targetNode.inputs[toPort] = `_CONNECTION_${connection.id}`; } connectionStart = null; canvas.style.cursor = 'grab'; renderCanvas(); } function renderNode(node) { const isSelected = selectedNode && selectedNode.id === node.id; const selectedClass = isSelected ? ' selected' : ''; let html = `
${node.definition.name}
`; // Input section if (Object.keys(node.definition.inputs).length > 0) { html += `
Inputs
`; Object.keys(node.definition.inputs).forEach(key => { const input = node.definition.inputs[key]; const value = node.inputs[key] || input.default || ''; html += `
${input.name}
`; }); html += `
`; } // Output section if (Object.keys(node.definition.outputs).length > 0) { html += `
Outputs
`; Object.keys(node.definition.outputs).forEach(key => { const output = node.definition.outputs[key]; html += `
${output.name}
`; }); html += `
`; } html += `
`; return html; } function getPortPosition(node, portKey, portType) { const nodeRect = { x: node.x + viewOffset.x, y: node.y + viewOffset.y, width: 200 }; const headerHeight = 40; const contentPadding = 15; let yOffset = headerHeight + contentPadding + 30; // Section title if (portType === 'input') { const inputKeys = Object.keys(node.definition.inputs); const portIndex = inputKeys.indexOf(portKey); yOffset += portIndex * 36; // parameter height return { x: nodeRect.x, y: nodeRect.y + yOffset + 18 // center of parameter }; } else { // Skip inputs section if it exists if (Object.keys(node.definition.inputs).length > 0) { yOffset += Object.keys(node.definition.inputs).length * 36 + 35; // inputs + section spacing } const outputKeys = Object.keys(node.definition.outputs); const portIndex = outputKeys.indexOf(portKey); yOffset += portIndex * 36; return { x: nodeRect.x + nodeRect.width, y: nodeRect.y + yOffset + 18 }; } } function createConnectionCurve(from, to) { const dx = to.x - from.x; const controlOffset = Math.max(100, Math.abs(dx) / 2); return `M ${from.x} ${from.y} C ${from.x + controlOffset} ${from.y}, ${to.x - controlOffset} ${to.y}, ${to.x} ${to.y}`; } // Event handlers function handleCanvasMouseDown(e) { updateCanvasRect(); const target = e.target; if (target.classList.contains('port')) { handlePortMouseDown(e, target); } else if (target.getAttribute('data-action') === 'drag-node') { handleNodeDragStart(e, target); } else if (target.closest('.node')) { handleNodeClick(e, target); } else { // Start canvas drag isCanvasDragging = true; lastMousePos = { x: e.clientX, y: e.clientY }; canvas.style.cursor = 'grabbing'; selectedNode = null; updatePropertiesPanel(); renderCanvas(); } e.preventDefault(); } function handleCanvasMouseMove(e) { if (isCanvasDragging) { const dx = e.clientX - lastMousePos.x; const dy = e.clientY - lastMousePos.y; viewOffset.x += dx; viewOffset.y += dy; lastMousePos = { x: e.clientX, y: e.clientY }; renderCanvas(); } } function handleCanvasMouseUp(e) { if (isCanvasDragging) { isCanvasDragging = false; canvas.style.cursor = 'grab'; } } function handleCanvasClick(e) { // This is handled in mousedown to prevent issues with dragging } function handlePortMouseDown(e, port) { const nodeId = parseInt(port.getAttribute('data-node-id')); const portKey = port.getAttribute('data-port'); const portType = port.getAttribute('data-port-type'); if (portType === 'output') { connectionStart = { nodeId, portKey, portType }; canvas.style.cursor = 'crosshair'; } e.stopPropagation(); } function handleNodeDragStart(e, header) { const nodeElement = header.closest('.node'); const nodeId = parseInt(nodeElement.getAttribute('data-node-id')); draggedNode = nodes.find(n => n.id === nodeId); if (draggedNode) { selectNode(draggedNode); const rect = nodeElement.getBoundingClientRect(); draggedNode.dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; } e.stopPropagation(); } function handleNodeClick(e, target) { const nodeElement = target.closest('.node'); if (nodeElement) { const nodeId = parseInt(nodeElement.getAttribute('data-node-id')); const node = nodes.find(n => n.id === nodeId); if (node) { selectNode(node); } } e.stopPropagation(); } function handleGlobalMouseMove(e) { if (draggedNode && draggedNode.dragOffset) { draggedNode.x = e.clientX - canvasRect.left - draggedNode.dragOffset.x - viewOffset.x; draggedNode.y = e.clientY - canvasRect.top - draggedNode.dragOffset.y - viewOffset.y; renderCanvas(); } // Handle connection preview if (connectionStart) { // Add preview line rendering here if needed } } function handleGlobalMouseUp(e) { const target = e.target; // Handle connection completion if (connectionStart && target.classList.contains('port')) { const toNodeId = parseInt(target.getAttribute('data-node-id')); const toPortKey = target.getAttribute('data-port'); const toPortType = target.getAttribute('data-port-type'); if (toPortType === 'input' && toNodeId !== connectionStart.nodeId) { // Create connection const connection = { fromNodeId: connectionStart.nodeId, fromPort: connectionStart.portKey, toNodeId: toNodeId, toPort: toPortKey, id: Date.now() }; // Remove existing connection to this input connections = connections.filter(c => !(c.toNodeId === toNodeId && c.toPort === toPortKey) ); connections.push(connection); // Update the target node's input value to reference the connection const targetNode = nodes.find(n => n.id === toNodeId); if (targetNode) { targetNode.inputs[toPortKey] = `_CONNECTION_${connection.id}`; } renderCanvas(); } } // Reset states connectionStart = null; canvas.style.cursor = 'grab'; if (draggedNode) { draggedNode.dragOffset = null; draggedNode = null; } } function deleteConnection(connectionId) { const connection = connections.find(c => c.id === connectionId); if (connection) { const targetNode = nodes.find(n => n.id === connection.toNodeId); if (targetNode) { const inputDef = targetNode.definition.inputs[connection.toPort]; targetNode.inputs[connection.toPort] = inputDef.default || '0'; } connections = connections.filter(c => c.id !== connectionId); renderCanvas(); } } function deleteNode(nodeId) { // Remove connections connections = connections.filter(c => c.fromNodeId !== nodeId && c.toNodeId !== nodeId ); // Remove node nodes = nodes.filter(n => n.id !== nodeId); if (selectedNode && selectedNode.id === nodeId) { selectedNode = null; updatePropertiesPanel(); } renderCanvas(); } // Export functionality function exportPatch() { let patch = ''; nodes.forEach(node => { patch += `[${node.type}]\n`; // Add inputs Object.keys(node.inputs).forEach(key => { const value = node.inputs[key]; if (value && value !== '0') { // Check if this is a connection if (value.startsWith('_CONNECTION_')) { const connectionId = parseInt(value.replace('_CONNECTION_', '')); const connection = connections.find(c => c.id === connectionId); if (connection) { const fromNode = nodes.find(n => n.id === connection.fromNodeId); if (fromNode) { const cableName = `_${fromNode.type.toUpperCase()}_${connection.fromPort.toUpperCase()}`; patch += `${key} = ${cableName}\n`; } } } else { patch += `${key} = ${value}\n`; } } }); // Add outputs that are connected Object.keys(node.definition.outputs).forEach(outputKey => { const connection = connections.find(c => c.fromNodeId === node.id && c.fromPort === outputKey ); if (connection) { const cableName = `_${node.type.toUpperCase()}_${outputKey.toUpperCase()}`; patch += `${outputKey} = ${cableName}\n`; } }); patch += '\n'; }); // Show export dialog const exportDialog = document.createElement('div'); exportDialog.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 10000; `; exportDialog.innerHTML = `

Exported DROID Patch

`; document.body.appendChild(exportDialog); } function clearCanvas() { if (confirm('Clear all nodes and connections?')) { nodes = []; connections = []; selectedNode = null; updatePropertiesPanel(); renderCanvas(); } } function loadExamplePatch() { clearCanvas(); // Create example patch: LFO -> Mixer -> Contour const lfo = { id: ++nodeIdCounter, type: 'lfo', x: 50, y: 50, inputs: { hz: '2', level: '1', bipolar: '0' }, outputs: {}, definition: circuitDefinitions.lfo }; const mixer = { id: ++nodeIdCounter, type: 'mixer', x: 350, y: 100, inputs: { input1: '_CONNECTION_1', input2: '0.5', input3: '0', input4: '0' }, outputs: {}, definition: circuitDefinitions.mixer }; const contour = { id: ++nodeIdCounter, type: 'contour', x: 650, y: 50, inputs: { trigger: '_CONNECTION_2', attack: '0.1', decay: '0.2', sustain: '0.6', release: '0.3' }, outputs: {}, definition: circuitDefinitions.contour }; nodes = [lfo, mixer, contour]; connections = [ { id: 1, fromNodeId: lfo.id, fromPort: 'square', toNodeId: mixer.id, toPort: 'input1' }, { id: 2, fromNodeId: mixer.id, fromPort: 'output', toNodeId: contour.id, toPort: 'trigger' } ]; renderCanvas(); selectNode(lfo); } // Add keyboard shortcuts document.addEventListener('keydown', function(e) { if (e.key === 'Delete' && selectedNode) { deleteNode(selectedNode.id); } else if (e.key === 'Escape') { selectedNode = null; updatePropertiesPanel(); renderCanvas(); } }); // Right-click context menu document.addEventListener('contextmenu', function(e) { const nodeElement = e.target.closest('.node'); if (nodeElement) { e.preventDefault(); const nodeId = parseInt(nodeElement.getAttribute('data-node-id')); const menu = document.createElement('div'); menu.style.cssText = ` position: fixed; left: ${e.clientX}px; top: ${e.clientY}px; background: #2a2a3c; border: 1px solid #444; border-radius: 6px; padding: 8px 0; z-index: 10000; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); `; menu.innerHTML = `
Delete Node
`; document.body.appendChild(menu); setTimeout(() => { document.addEventListener('click', function removeMenu() { menu.remove(); document.removeEventListener('click', removeMenu); }); }, 100); } });