feat: buffer/latency control + non-selectable node text

Buffer Control:
- GET /api/quantum - reads current graph quantum via pw-metadata
- POST /api/quantum {quantum:N} - sets quantum via pw-metadata
- Toolbar dropdown with presets: 32, 64, 128, 256, 512, 1024, 2048, 4096
- Loads current quantum on startup

Text Selection Fix:
- Added user-select: none to wrap and canvas CSS
- Node text no longer gets selected when dragging
This commit is contained in:
joren
2026-03-30 01:10:46 +02:00
parent 839eb5156a
commit f76583a33e
3 changed files with 77 additions and 3 deletions

View File

@@ -13,6 +13,7 @@
saveProfile, loadProfile, deleteProfile,
setNodeVolume, setNodeMute,
createNullSink, createLoopback, loadModule,
getQuantum, setQuantum,
} from '../lib/stores';
import type { Node, Port, Link } from '../lib/types';
@@ -50,6 +51,7 @@
// Positions
let nodePositions = $state<Record<string, { x: number; y: number }>>({});
let currentQuantum = $state(0);
const POS_KEY = 'pwweb_positions';
function loadPositions() {
@@ -420,7 +422,7 @@
}
}
onMount(() => { initGraph(); loadPositions(); });
onMount(() => { initGraph(); loadPositions(); getQuantum().then(q => { currentQuantum = q; }); });
onDestroy(() => { destroyGraph(); });
</script>
@@ -460,6 +462,15 @@
<button onclick={() => { showRuleDialog = !showRuleDialog; showHideDialog = false; showMergeDialog = false; showProfileDialog = false; }} title="Manage patchbay rules">Rules</button>
<button onclick={() => { showProfileDialog = !showProfileDialog; showHideDialog = false; showMergeDialog = false; showRuleDialog = false; }} title="Save/load profiles">Profiles</button>
<button onclick={() => { showVirtualMenu = !showVirtualMenu; showHideDialog = false; showMergeDialog = false; showProfileDialog = false; showRuleDialog = false; }} title="Add virtual device">+ Add</button>
<span class="sep"></span>
<label class="quantum-label">Buffer:
<select class="quantum-select" onchange={(e) => { const q = Number((e.target as HTMLSelectElement).value); if (q > 0) { currentQuantum = q; setQuantum(q); } }}>
<option value="0" selected={currentQuantum === 0}>default</option>
{#each [32, 64, 128, 256, 512, 1024, 2048, 4096] as q}
<option value={q} selected={currentQuantum === q}>{q}</option>
{/each}
</select>
</label>
<span class="stats">{$nodes.length}N {$ports.length}P {$links.length}L {#if $patchbay.pinned_connections.length > 0}{$patchbay.pinned_connections.length}p{/if}</span>
</div>
@@ -821,8 +832,8 @@
</div>
<style>
.wrap { width: 100%; height: 100vh; background: #14141e; position: relative; overflow: hidden; }
.canvas { width: 100%; height: 100%; display: block; cursor: default; position: absolute; top: 0; left: 0; z-index: 1; }
.wrap { width: 100%; height: 100vh; background: #14141e; position: relative; overflow: hidden; user-select: none; -webkit-user-select: none; }
.canvas { width: 100%; height: 100%; display: block; cursor: default; position: absolute; top: 0; left: 0; z-index: 1; user-select: none; -webkit-user-select: none; }
.toolbar {
position: absolute; top: 0; left: 0; right: 0; z-index: 10;
@@ -859,6 +870,12 @@
.toolbar button:hover, .toggle:hover { background: #3a3a4e; }
.toggle.active { background: #1a3a2a; border-color: #4a9; color: #6c9; }
.quantum-label { font-size: 10px; color: #888; display: flex; align-items: center; gap: 4px; }
.quantum-select {
padding: 1px 4px; background: #1a1a2e; border: 1px solid #444;
color: #aaa; font-size: 10px; border-radius: 3px; font-family: monospace;
cursor: pointer;
}
.stats { margin-left: auto; color: #555; }
/* Context menu */

View File

@@ -441,4 +441,25 @@ export async function unloadModule(moduleId: number) {
}
}
// Quantum (buffer size) control
export async function getQuantum(): Promise<number> {
try {
const res = await fetch('/api/quantum');
const data = await res.json();
return data.quantum || 0;
} catch { return 0; }
}
export async function setQuantum(quantum: number) {
try {
await fetch('/api/quantum', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantum }),
});
} catch (e) {
console.error('[api] set-quantum failed:', e);
}
}
export { connectPorts, disconnectPorts };

View File

@@ -603,6 +603,42 @@ void WebServer::setupRoutes() {
}
res.set_header("Access-Control-Allow-Origin", "*");
});
m_http.Options("/api/quantum", cors_handler);
// Get current quantum: GET /api/quantum
m_http.Get("/api/quantum", [](const httplib::Request &, httplib::Response &res) {
int quantum = 0;
FILE *fp = popen("pw-metadata 0 default.clock.quantum 2>/dev/null | grep -oP \"value:'\\K[0-9]+\"", "r");
if (fp) {
char buf[32] = {};
if (fgets(buf, sizeof(buf), fp))
quantum = atoi(buf);
pclose(fp);
}
char out[64];
snprintf(out, sizeof(out), "{\"quantum\":%d}", quantum);
res.set_content(out, "application/json");
res.set_header("Access-Control-Allow-Origin", "*");
});
// Set quantum: POST /api/quantum {"quantum":256}
m_http.Post("/api/quantum", [](const httplib::Request &req, httplib::Response &res) {
int quantum = 0;
if (sscanf(req.body.c_str(), "{\"quantum\":%d}", &quantum) == 1 && quantum > 0) {
char cmd[128];
snprintf(cmd, sizeof(cmd), "pw-metadata 0 default.clock.quantum %d 2>/dev/null", quantum);
int ret = system(cmd);
(void)ret;
char out[64];
snprintf(out, sizeof(out), "{\"ok\":true,\"quantum\":%d}", quantum);
res.set_content(out, "application/json");
} else {
res.status = 400;
res.set_content("{\"error\":\"invalid json\"}", "application/json");
}
res.set_header("Access-Control-Allow-Origin", "*");
});
}
// end of web_server.cpp