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()}")