diff --git a/webapp/path_navigator.html b/webapp/path_navigator.html index 560d752..987988d 100644 --- a/webapp/path_navigator.html +++ b/webapp/path_navigator.html @@ -271,8 +271,9 @@

Path Navigator

- Upload file: - + File: + + Fundamental (Hz):
@@ -316,7 +317,7 @@ // Graph dimensions - will expand based on number of chords let graphWidth = 1100; const graphHeight = 450; - const chordSpacing = 350; + let chordSpacing = 350; // Voice colors - sleek pastel scheme const voiceColors = ['#7eb5a6', '#c5a3ff', '#ffb3b3', '#ffd700']; @@ -374,14 +375,15 @@ layout: { name: 'preset', }, - minZoom: 1, - maxZoom: 1, - zoomingEnabled: false, - panningEnabled: false, + minZoom: 0.3, + maxZoom: 3, + zoomingEnabled: true, + panningEnabled: true, + wheelSensitivity: 0.2, // Reduce wheel zoom sensitivity autounselectify: true, boxSelectionEnabled: false, - }); - + }); + // Lock y on drag - only allow x movement let isDragging = false; let grabPosition = null; @@ -450,102 +452,6 @@ }); } - // Render all three chords using Cytoscape (legacy - not used with all-chords approach) - function renderAllThree(prevData, currentData, nextData) { - if (!cy) initCytoscapeAll(3); - - // Clear previous elements - cy.elements().remove(); - - // Build elements array - let elements = []; - - // Collect all chords - const allChords = []; - if (prevData && prevData.nodes) allChords.push({data: prevData, xPos: graphWidth * 0.2, label: "prev"}); - if (currentData && currentData.nodes) allChords.push({data: currentData, xPos: graphWidth * 0.5, label: "current"}); - if (nextData && nextData.nodes) allChords.push({data: nextData, xPos: graphWidth * 0.8, label: "next"}); - - // Collect all cents from all chords for global scale - let allCents = []; - allChords.forEach(c => { - allCents = allCents.concat(c.data.nodes.map(n => n.cents)); - }); - const globalMinCents = Math.min(...allCents); - const globalMaxCents = Math.max(...allCents); - const globalCentsRange = globalMaxCents - globalMinCents || 1; - const ySpread = graphHeight * 0.8; - const yBase = graphHeight * 0.1; - - // Add nodes for each chord - let nodeOffset = 0; - allChords.forEach(({data, xPos, label: chordLabel}) => { - const nodes = data.nodes; - const edges = data.edges || []; - const idMap = {}; - - nodes.forEach((n, i) => { - const globalId = nodeOffset + n.id; - idMap[n.id] = globalId; - - // Wider initial spread: 25% of graphWidth - const x = xPos + (graphWidth * 0.25) * (i / (nodes.length - 1 || 1)); - const y = yBase + ySpread - ((n.cents - globalMinCents) / globalCentsRange) * ySpread; - - elements.push({ - group: 'nodes', - data: { - id: String(globalId), - localId: n.id, - cents: n.cents, - fraction: n.fraction, - chordLabel: chordLabel, - color: voiceColors[n.id % voiceColors.length], - }, - position: { x: x, y: y }, - }); - }); - - // Add edges - edges.forEach(e => { - elements.push({ - group: 'edges', - data: { - source: String(idMap[e.source]), - target: String(idMap[e.target]), - ratio: e.ratio, - }, - }); - }); - - nodeOffset += nodes.length; - }); - - if (elements.length === 0) return; - - // Add elements to Cytoscape - cy.add(elements); - - // Run xforce layout to optimize x positions while keeping y fixed - // Bounds: each chord stays in its third of the container - cy.layout({ - name: 'xforce', - linkDistance: 60, - linkStrength: 0.1, - charge: -60, - collisionDistance: 35, - damping: 0.7, - iterations: 150, - bounds: { - 'prev': { min: 50, max: 400 }, - 'current': { min: 350, max: 750 }, - 'next': { min: 700, max: 1050 }, - }, - }).run(); - - cy.fit(); - } - // Render ALL chords at once for continuous canvas function renderAllChords() { if (!allGraphsData || !cy) return; @@ -717,57 +623,6 @@ } } - // Load from Flask API - get computed graph from Python (legacy) - async function loadFromAPI() { - try { - const response = await fetch(`/api/graph/${currentIndex}`); - if (!response.ok) throw new Error("API not available"); - const data = await response.json(); - - currentIndex = data.index; - totalSteps = data.total - 1; - hasPrev = data.has_prev; - hasNext = data.has_next; - - // Render all three graphs in single panel - renderAllThree(data.prev, data.current, data.next); - - // Update displays - document.getElementById("currentIndex").textContent = currentIndex; - document.getElementById("totalSteps").textContent = totalSteps; - document.getElementById("prevBtn").disabled = !hasPrev; - document.getElementById("nextBtn").disabled = !hasNext; - - // Update chord panels - updateChordPanel("prevPitches", data.prev ? data.prev.nodes : null); - updateChordPanel("currentPitches", data.current ? data.current.nodes : null); - updateChordPanel("nextPitches", data.next ? data.next.nodes : null); - - } catch (e) { - console.log("API not available, trying local file", e); - loadFromLocalFile(); - } - } - - async function setFundamental() { - const input = document.getElementById("fundamentalInput"); - const fundamental = parseFloat(input.value); - if (!fundamental || fundamental <= 0) { - return; - } - try { - const response = await fetch("/api/set-fundamental", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({ fundamental: fundamental }) - }); - const data = await response.json(); - console.log("Fundamental set to:", data.fundamental, "Hz"); - } catch (e) { - console.log("Error setting fundamental", e); - } - } - // Fundamental input - auto-send on Enter, Up/Down arrows, or input change document.getElementById("fundamentalInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { @@ -791,39 +646,34 @@ setFundamental(); }); - async function loadFromLocalFile() { - try { - const response = await fetch("output/output_chords.json"); - if (!response.ok) throw new Error("File not found"); - const data = await response.json(); - loadChords(data.chords); - } catch (e) { - console.log("No default file, waiting for user upload"); - } - } - // File input handler - document.getElementById("fileInput").addEventListener("change", async (e) => { - const file = e.target.files[0]; - if (!file) return; + document.getElementById("loadFileBtn").addEventListener("click", async () => { + const filepath = document.getElementById("filepathInput").value; + if (!filepath) return; - const reader = new FileReader(); - reader.onload = (event) => { - try { - const data = JSON.parse(event.target.result); - loadChords(data.chords || data); - } catch (err) { - alert("Invalid JSON file"); + try { + const response = await fetch("/api/load-file", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ filepath: filepath }) + }); + const data = await response.json(); + if (data.error) { + alert(data.error); + } else { + loadAllGraphs(); } - }; - reader.readAsText(file); + } catch (err) { + alert("Error loading file: " + err); + } }); - function loadChords(chordData) { - chords = chordData; - currentIndex = 0; - updateDisplay(); - } + // Also allow Enter key in filepath input + document.getElementById("filepathInput").addEventListener("keydown", (e) => { + if (e.key === "Enter") { + document.getElementById("loadFileBtn").click(); + } + }); function updateChordPanel(elementId, data) { const container = document.getElementById(elementId); @@ -894,11 +744,6 @@ container.appendChild(table); } - // Update display - now using pan navigation with all chords loaded - function updateDisplay() { - updateUI(); - } - // Navigation with pan animation async function navigate(direction) { if (direction === 'prev' && currentIndex > 0) { @@ -931,6 +776,18 @@ navigate('prev'); } else if (e.key === "ArrowRight") { navigate('next'); + } else if (e.key === "+" || e.key === "=") { + // Zoom in + if (cy) { + const zoom = cy.zoom(); + cy.zoom({ zoomLevel: Math.min(3, zoom * 1.1), renderedPosition: { x: cy.width()/2, y: cy.height()/2 } }); + } + } else if (e.key === "-") { + // Zoom out + if (cy) { + const zoom = cy.zoom(); + cy.zoom({ zoomLevel: Math.max(0.3, zoom / 1.1), renderedPosition: { x: cy.width()/2, y: cy.height()/2 } }); + } } else if (e.key === "k") { // Soft kill - send 20 Hz to stop voices gently fetch('/api/kill-siren', { diff --git a/webapp/server.py b/webapp/server.py index b42229d..47942af 100644 --- a/webapp/server.py +++ b/webapp/server.py @@ -385,6 +385,27 @@ def kill_siren(): ) +@app.route("/api/load-file", methods=["POST"]) +def load_file(): + global current_index, chords + data = request.json + filepath = data.get("filepath") + + if not filepath: + return jsonify({"error": "No filepath provided"}), 400 + + try: + with open(filepath) as f: + chords_data = json.load(f) + chords = chords_data.get("chords", []) + current_index = 0 + return jsonify({"loaded": len(chords), "filepath": filepath}) + except FileNotFoundError: + return jsonify({"error": f"File not found: {filepath}"}), 404 + except json.JSONDecodeError: + return jsonify({"error": f"Invalid JSON in: {filepath}"}), 400 + + if __name__ == "__main__": print("Starting Path Navigator server...") print(f"Loading chords from: {get_chords_file()}")