Merge feature/graph-ux-meters into master
This commit is contained in:
@@ -139,17 +139,20 @@
|
|||||||
return $patchbay.aliases?.[nd.name] || nd.nick || nd.name;
|
return $patchbay.aliases?.[nd.name] || nd.nick || nd.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level meter helpers
|
// Graph visibility toggle
|
||||||
function meterDb(nodeId: number): number {
|
let showGraph = $state(true);
|
||||||
|
|
||||||
|
// Level meter: returns how many of `total` segments should be lit
|
||||||
|
function meterSegs(nodeId: number, total: number): number {
|
||||||
const p = $peaks[nodeId] ?? 0;
|
const p = $peaks[nodeId] ?? 0;
|
||||||
return p > 0 ? Math.max(-60, 20 * Math.log10(p)) : -60;
|
if (p <= 0) return 0;
|
||||||
|
const db = Math.max(-60, 20 * Math.log10(p));
|
||||||
|
return Math.round(Math.max(0, (db + 60) / 60) * total);
|
||||||
}
|
}
|
||||||
function meterWidth(nodeId: number, barW: number): number {
|
// Segment color by position (0 = leftmost/quietest)
|
||||||
return barW * Math.max(0, (meterDb(nodeId) + 60) / 60);
|
function segColor(i: number, total: number): string {
|
||||||
}
|
const pct = i / total;
|
||||||
function meterColor(nodeId: number): string {
|
return pct >= 0.90 ? '#c44' : pct >= 0.75 ? '#ca4' : '#4a9';
|
||||||
const db = meterDb(nodeId);
|
|
||||||
return db >= -3 ? '#c44' : db >= -12 ? '#ca4' : '#4a9';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build computed layout
|
// Build computed layout
|
||||||
@@ -207,7 +210,7 @@
|
|||||||
const headerH = 22;
|
const headerH = 22;
|
||||||
const portH = 16;
|
const portH = 16;
|
||||||
const maxPorts = Math.max(allInPorts.length, allOutPorts.length, 1);
|
const maxPorts = Math.max(allInPorts.length, allOutPorts.length, 1);
|
||||||
const height = headerH + maxPorts * portH + 28; // 20 for volume slider + 8 for meter bar
|
const height = headerH + maxPorts * portH + 36; // 20 for volume slider + 16 for VU meter
|
||||||
const width = 220;
|
const width = 220;
|
||||||
|
|
||||||
const portPositions = new Map<number, { x: number; y: number }>();
|
const portPositions = new Map<number, { x: number; y: number }>();
|
||||||
@@ -507,6 +510,8 @@
|
|||||||
</label>
|
</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>
|
<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>
|
||||||
|
<span class="sep"></span>
|
||||||
|
<button class="toggle" class:active={showGraph} onclick={() => showGraph = !showGraph} title="Show/hide graph canvas">Graph</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Virtual device dropdown -->
|
<!-- Virtual device dropdown -->
|
||||||
@@ -551,6 +556,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
{#if showGraph}
|
||||||
<svg
|
<svg
|
||||||
bind:this={svgEl}
|
bind:this={svgEl}
|
||||||
viewBox="{viewBox.x} {viewBox.y} {viewBox.w} {viewBox.h}"
|
viewBox="{viewBox.x} {viewBox.y} {viewBox.w} {viewBox.h}"
|
||||||
@@ -690,13 +696,22 @@
|
|||||||
{Math.round(Math.max(0, Math.min(1, nd.volume)) * 100)}%
|
{Math.round(Math.max(0, Math.min(1, nd.volume)) * 100)}%
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
<!-- Level meter -->
|
<!-- VU meter: 20 segmented LEDs -->
|
||||||
<rect x={nd.x + 8} y={nd.y + nd.height - 6} width={nd.width - 16} height="3" rx="1" fill="#1a1a1a" />
|
{#each Array(20) as _, i}
|
||||||
<rect x={nd.x + 8} y={nd.y + nd.height - 6} width={meterWidth(nd.id, nd.width - 16)} height="3" rx="1" fill={meterColor(nd.id)} />
|
<rect
|
||||||
|
x={nd.x + 8 + i * ((nd.width - 16) / 20)}
|
||||||
|
y={nd.y + nd.height - 13}
|
||||||
|
width={(nd.width - 16) / 20 - 1.5}
|
||||||
|
height="10"
|
||||||
|
rx="1"
|
||||||
|
fill={i < meterSegs(nd.id, 20) ? segColor(i, 20) : '#1e1e28'}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
|
||||||
</g>
|
</g>
|
||||||
{/each}
|
{/each}
|
||||||
</svg>
|
</svg>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Context menu -->
|
<!-- Context menu -->
|
||||||
{#if contextMenu}
|
{#if contextMenu}
|
||||||
@@ -908,11 +923,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wrap { width: 100%; height: 100vh; background: #14141e; position: relative; overflow: hidden; user-select: none; -webkit-user-select: none; }
|
.wrap { width: 100%; height: 100vh; background: #14141e; display: flex; flex-direction: column; 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; }
|
.canvas { flex: 1; min-height: 0; width: 100%; display: block; cursor: default; z-index: 1; user-select: none; -webkit-user-select: none; }
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
position: absolute; top: 0; left: 0; right: 0; z-index: 10;
|
position: relative; flex-shrink: 0; z-index: 10;
|
||||||
display: flex; align-items: center; gap: 6px;
|
display: flex; align-items: center; gap: 6px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
background: rgba(16, 16, 24, 0.97);
|
background: rgba(16, 16, 24, 0.97);
|
||||||
|
|||||||
Reference in New Issue
Block a user