diff --git a/src/App.jsx b/src/App.jsx index a7beaaa..0f884eb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,13 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { state, subscribe, addModule, emit, addConnection, updateModulePosition } from './engine/state.js'; -import { startAudio, stopAudio, connectWire, rebuildGraph, getAudioNode } from './engine/audioEngine.js'; -import { getModuleDef, PORT_TYPE } from './engine/moduleRegistry.js'; +import { state, subscribe, addModule, emit, addConnection, updateModulePosition, deserialize } from './engine/state.js'; +import { startAudio, stopAudio, connectWire, rebuildGraph } from './engine/audioEngine.js'; +import { getModuleDef } from './engine/moduleRegistry.js'; import { autoSave, autoLoad, exportPatch, importPatch } from './engine/presets.js'; import ModuleNode from './components/ModuleNode.jsx'; import WireLayer from './components/WireLayer.jsx'; import ModulePalette from './components/ModulePalette.jsx'; import PresetModal from './components/PresetModal.jsx'; +import { CHIPTUNE_PRESET } from './presets/chiptune.js'; export default function App() { const [, forceUpdate] = useState(0); @@ -23,11 +24,12 @@ export default function App() { return unsub; }, []); - // Auto-load on mount + // Auto-load on mount, or load chiptune demo if empty useEffect(() => { const loaded = autoLoad(); - if (loaded && state.isRunning) { - startAudio(); + if (!loaded || state.modules.length === 0) { + // Load chiptune demo preset + deserialize(CHIPTUNE_PRESET); } }, []); @@ -56,43 +58,48 @@ export default function App() { }); }, []); + // Find port-dot element at pointer position (including nearby) + const findPortAtPoint = (x, y) => { + // First try exact hit + const el = document.elementFromPoint(x, y); + if (el && el.classList.contains('port-dot') && el.dataset.moduleId) { + return el; + } + // Try a small radius around the point + for (const [dx, dy] of [[0,0],[-8,0],[8,0],[0,-8],[0,8],[-6,-6],[6,-6],[-6,6],[6,6]]) { + const hit = document.elementFromPoint(x + dx, y + dy); + if (hit && hit.classList.contains('port-dot') && hit.dataset.moduleId) { + return hit; + } + } + return null; + }; + // Canvas pointer events const handlePointerDown = useCallback((e) => { - if (e.button === 1 || (e.button === 0 && e.altKey)) { - // Middle click or Alt+click: start panning - state.panning = true; - state.panStart = { x: e.clientX - state.camX, y: e.clientY - state.camY }; - e.preventDefault(); - } else if (e.button === 2) { - // Right click: pan + if (e.button === 1 || (e.button === 0 && e.altKey) || e.button === 2) { state.panning = true; state.panStart = { x: e.clientX - state.camX, y: e.clientY - state.camY }; e.preventDefault(); } else if (e.button === 0 && !connectingRef.current) { - // Left click on empty space: deselect state.selectedModuleId = null; emit(); } }, []); const handlePointerMove = useCallback((e) => { - // Panning if (state.panning && state.panStart) { state.camX = e.clientX - state.panStart.x; state.camY = e.clientY - state.panStart.y; emit(); return; } - - // Module dragging if (state.dragging) { const newX = e.clientX / state.zoom - state.dragging.offsetX; const newY = e.clientY / state.zoom - state.dragging.offsetY; updateModulePosition(state.dragging.moduleId, newX, newY); return; } - - // Temp wire if (connectingRef.current && containerRef.current) { const containerRect = containerRef.current.getBoundingClientRect(); setTempWire(prev => prev ? { @@ -108,80 +115,35 @@ export default function App() { state.panning = false; state.panStart = null; } - if (state.dragging) { state.dragging = null; emit(); } - // End connecting โ check if we're over a port + // End connecting if (connectingRef.current) { - const target = document.elementFromPoint(e.clientX, e.clientY); - if (target && target.classList.contains('port-dot')) { - // Find which module/port this target belongs to - const moduleEl = target.closest('.module'); - if (moduleEl) { - finishConnection(target, e); - } + const portEl = findPortAtPoint(e.clientX, e.clientY); + if (portEl) { + finishConnection(portEl); } connectingRef.current = null; setTempWire(null); } }, []); - const finishConnection = (portEl, e) => { + const finishConnection = (portEl) => { const from = connectingRef.current; if (!from) return; - // Find target module and port by inspecting DOM - // Walk up to .module, find moduleId, then find port - const moduleEl = portEl.closest('.module'); - if (!moduleEl) return; + // Read data attributes directly โ clean and reliable + const targetModuleId = parseInt(portEl.dataset.moduleId); + const targetPort = portEl.dataset.portName; + const targetDirection = portEl.dataset.portDirection; - // Get all port-row elements to find index - const portRows = moduleEl.querySelectorAll('.port-row'); - let targetModuleId = null; - let targetPort = null; - let targetDirection = null; + if (!targetModuleId || !targetPort || !targetDirection) return; + if (targetModuleId === from.moduleId && targetPort === from.port) return; - for (const mod of state.modules) { - const def = getModuleDef(mod.type); - if (!def) continue; - - const modX = mod.x * state.zoom; - const modY = mod.y * state.zoom; - const containerRect = containerRef.current.getBoundingClientRect(); - const moduleRect = moduleEl.getBoundingClientRect(); - - // Check if this module element matches - const expectedLeft = modX + state.camX + containerRect.left; - if (Math.abs(moduleRect.left - expectedLeft) > 5) continue; - - targetModuleId = mod.id; - - // Find which port-dot was clicked - const allDots = moduleEl.querySelectorAll('.port-dot'); - const allInputs = def.inputs.map(p => p.name); - const allOutputs = def.outputs.map(p => p.name); - - allDots.forEach((dot, idx) => { - if (dot === portEl) { - if (idx < allInputs.length) { - targetPort = allInputs[idx]; - targetDirection = 'input'; - } else { - targetPort = allOutputs[idx - allInputs.length]; - targetDirection = 'output'; - } - } - }); - break; - } - - if (!targetModuleId || !targetPort) return; - if (targetModuleId === from.moduleId) return; // No self-connections - - // Determine from/to based on direction + // Determine from/to let fromMod, fromPort, toMod, toPort; if (from.direction === 'output' && targetDirection === 'input') { fromMod = from.moduleId; fromPort = from.port; @@ -190,7 +152,7 @@ export default function App() { fromMod = targetModuleId; fromPort = targetPort; toMod = from.moduleId; toPort = from.port; } else { - return; // Invalid: same direction + return; // same direction โ invalid } const connId = addConnection(fromMod, fromPort, toMod, toPort); @@ -203,14 +165,12 @@ export default function App() { const handleWheel = useCallback((e) => { e.preventDefault(); const delta = -e.deltaY * 0.001; - const newZoom = Math.max(0.3, Math.min(3, state.zoom + delta)); - state.zoom = newZoom; + state.zoom = Math.max(0.3, Math.min(3, state.zoom + delta)); emit(); }, []); const handleContextMenu = useCallback((e) => e.preventDefault(), []); - // Toolbar actions const handleToggleAudio = async () => { if (state.isRunning) { stopAudio(); @@ -223,10 +183,8 @@ export default function App() { const handleAddModule = (type) => { const x = (-state.camX + 300) / state.zoom + Math.random() * 50; const y = (-state.camY + 200) / state.zoom + Math.random() * 50; - const id = addModule(type, x, y); - if (state.isRunning) { - rebuildGraph(); - } + addModule(type, x, y); + if (state.isRunning) rebuildGraph(); }; const handleImport = async (e) => { @@ -237,6 +195,12 @@ export default function App() { e.target.value = ''; }; + const handleLoadDemo = () => { + deserialize(CHIPTUNE_PRESET); + if (state.isRunning) rebuildGraph(); + emit(); + }; + return (