@@ -1,4 +1,4 @@
import { useMemo } from 'react' ;
import { useEffect , useMemo , useRef , useState } from 'react' ;
import { InputChannel , OutputChannel } from '../../features/dsp408/types' ;
import { DSP408 } from '../../types' ;
import {
@@ -69,6 +69,14 @@ const MARGIN = 36;
const LEFT_CHAIN_W = 560 ;
const RIGHT_CHAIN_W = 620 ;
// Width of the invisible "hitbox" stroke drawn under each visible line, in SVG
// user units. Much wider than the visible 1– 2.5px line so it's easy to click
// or catch with a scroll wheel without needing pixel-perfect aim.
const LINE_HITBOX_WIDTH = 14 ;
// How long the gain tooltip stays visible after the last wheel tick, in ms.
const GAIN_TOOLTIP_HOLD_MS = 700 ;
function getRoutes ( dsp : DSP408 , output : OutputChannel ) {
return dsp . state . current_config . output_states [ output ] . matrix_routes ;
}
@@ -79,6 +87,18 @@ function getGain(dsp: DSP408, output: OutputChannel, input: InputChannel) {
return getRoutes ( dsp , output ) . gains [ input ] ;
}
function formatGainDb ( value : number ) {
const sign = value > 0 ? '+' : '' ;
return ` ${ sign } ${ value . toFixed ( 1 ) } dB ` ;
}
type GainTooltipState = {
visible : boolean ;
x : number ;
y : number ;
label : string ;
} ;
function SignalFlowMatrix ( {
dsp ,
setMatrixRouting ,
@@ -109,6 +129,20 @@ function SignalFlowMatrix({
const inputNodeX = leftStart + LEFT_CHAIN_W ;
const outputNodeX = rightStart ;
const [ gainTooltip , setGainTooltip ] = useState < GainTooltipState > ( {
visible : false ,
x : 0 ,
y : 0 ,
label : '' ,
} ) ;
const hideTooltipTimer = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
useEffect ( ( ) = > {
return ( ) = > {
if ( hideTooltipTimer . current ) clearTimeout ( hideTooltipTimer . current ) ;
} ;
} , [ ] ) ;
const handleToggle = ( output : OutputChannel , input : InputChannel ) = > {
const routes = getRoutes ( dsp , output ) ;
const next = routes . connected . includes ( input )
@@ -124,6 +158,19 @@ function SignalFlowMatrix({
const next =
Math . round ( Math . min ( MATRIX_GAIN_MAX , Math . max ( MATRIX_GAIN_MIN , current + delta ) ) * 10 ) / 10 ;
void setMatrixGain ( input , output , next ) ;
// Show a small tooltip with the new gain value right next to the cursor,
// then let it fade away shortly after wheeling stops.
if ( hideTooltipTimer . current ) clearTimeout ( hideTooltipTimer . current ) ;
setGainTooltip ( {
visible : true ,
x : e.clientX ,
y : e.clientY ,
label : formatGainDb ( next ) ,
} ) ;
hideTooltipTimer . current = setTimeout ( ( ) = > {
setGainTooltip ( ( t ) = > ( { . . . t , visible : false } ) ) ;
} , GAIN_TOOLTIP_HOLD_MS ) ;
} ;
const routePairs = useMemo ( ( ) = > {
@@ -134,6 +181,18 @@ function SignalFlowMatrix({
return pairs ;
} , [ ] ) ;
// Draw dashed (unconnected) links first so connected (solid) links always
// paint on top of them, regardless of input/output index order. This has
// to be recomputed whenever routing changes, since isRouted reads live
// dsp state — unlike routePairs above, which is just static index pairs.
const orderedRoutePairs = useMemo ( ( ) = > {
return [ . . . routePairs ] . sort ( ( a , b ) = > {
const aConnected = isRouted ( dsp , a . output , a . input ) ? 1 : 0 ;
const bConnected = isRouted ( dsp , b . output , b . input ) ? 1 : 0 ;
return aConnected - bConnected ;
} ) ;
} , [ routePairs , dsp ] ) ;
return (
< div className = "signal-flow" >
< svg
@@ -141,7 +200,7 @@ function SignalFlowMatrix({
preserveAspectRatio = "xMidYMid meet"
className = "signal-flow__svg"
>
{ routePairs . map ( ( { input , output , i , j } ) = > {
{ orderedRoutePairs . map ( ( { input , output , i , j } ) = > {
const connected = isRouted ( dsp , output , input ) ;
const y1 = inputCenterY ( i ) ;
const y2 = outputCenterY ( j ) ;
@@ -152,6 +211,20 @@ function SignalFlowMatrix({
return (
< g key = { ` ${ input } - ${ output } ` } >
{ /* Invisible wide-stroke hitbox: carries the click/wheel handlers so
the line is easy to grab without needing to hit the thin visible
stroke exactly. Sits directly under the visible path. */ }
< path
d = { path }
fill = "none"
stroke = "transparent"
strokeWidth = { LINE_HITBOX_WIDTH }
style = { { cursor : 'pointer' } }
onClick = { ( ) = > handleToggle ( output , input ) }
onWheel = { connected ? ( e ) = > handleWheelGain ( e , output , input ) : undefined }
/ >
{ /* Visible line, purely decorative — no pointer events, so it never
competes with the hitbox above for hover/click/wheel. */ }
< path
d = { path }
fill = "none"
@@ -159,9 +232,7 @@ function SignalFlowMatrix({
strokeWidth = { connected ? 2.5 : 1 }
strokeDasharray = { connected ? undefined : '4 5' }
opacity = { connected ? 0.9 : 0.35 }
style = { { cursor : 'pointer' } }
onClick = { ( ) = > handleToggle ( output , input ) }
onWheel = { connected ? ( e ) = > handleWheelGain ( e , output , input ) : undefined }
pointerEvents = "none"
/ >
< / g >
) ;
@@ -252,8 +323,32 @@ function SignalFlowMatrix({
) ;
} ) }
< / svg >
{ gainTooltip . visible && (
< div
className = "signal-flow__gain-tooltip"
style = { {
position : 'fixed' ,
left : gainTooltip.x + 14 ,
top : gainTooltip.y - 28 ,
pointerEvents : 'none' ,
zIndex : 50 ,
padding : '2px 8px' ,
borderRadius : 6 ,
fontSize : 12 ,
fontVariantNumeric : 'tabular-nums' ,
background : 'var(--surface-elevated, rgba(20, 20, 24, 0.92))' ,
color : 'var(--text-primary, #fff)' ,
border : '1px solid var(--border-hairline, #333)' ,
boxShadow : '0 2px 8px rgba(0,0,0,0.35)' ,
whiteSpace : 'nowrap' ,
} }
>
{ gainTooltip . label }
< / div >
) }
< / div >
) ;
}
export default SignalFlowMatrix ;
export default SignalFlowMatrix ;