refactor(28-02): rewrite SessionTrackVU with direct DOM updates
- Remove useVuContext - directly import vuStore - Remove VuMeter component usage - render lights directly - Add RAF loop polling vuStore.getLevelSnapshot at ~60fps - Store light element refs for direct className assignment - Wrap with React.memo to prevent parent re-renders - Zero React re-renders for VU visual updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f8214854d9
commit
a020e27260
|
|
@ -1,44 +1,102 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { useVuContext } from '../../context/VuContext';
|
||||
import React, { useEffect, useRef, memo } from 'react';
|
||||
import { vuStore } from '../../stores/vuStore';
|
||||
|
||||
function SessionTrackVU({ lightCount, orientation, lightWidth, lightHeight, side, ptr, mixers }) {
|
||||
const { VuMeter, updateVuState } = useVuContext();
|
||||
const ptrRef = useRef(ptr || `STV${Date.now()}`);
|
||||
const SessionTrackVU = memo(function SessionTrackVU({
|
||||
lightCount = 16,
|
||||
orientation = 'vertical',
|
||||
lightWidth = 15,
|
||||
lightHeight = 10,
|
||||
mixers
|
||||
}) {
|
||||
const containerRef = useRef(null);
|
||||
const lightsRef = useRef([]);
|
||||
const mixerIdRef = useRef(null);
|
||||
const rafIdRef = useRef(null);
|
||||
|
||||
// Update mixerId when mixers prop changes
|
||||
useEffect(() => {
|
||||
const mixer = mixers?.vuMixer;
|
||||
mixerIdRef.current = mixer ? `${mixer.mode ? 'M' : 'P'}${mixer.id}` : null;
|
||||
|
||||
if (!mixer) {
|
||||
mixerIdRef.current = null;
|
||||
return () => {
|
||||
// Cleanup VU state on unmount
|
||||
if (mixerIdRef.current) {
|
||||
vuStore.removeLevel(mixerIdRef.current);
|
||||
}
|
||||
};
|
||||
}, [mixers]);
|
||||
|
||||
// Animation loop for direct DOM updates - separate from React lifecycle
|
||||
useEffect(() => {
|
||||
const updateVisuals = () => {
|
||||
const mixerId = mixerIdRef.current;
|
||||
|
||||
if (!mixerId) {
|
||||
rafIdRef.current = requestAnimationFrame(updateVisuals);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a unique ID for this VU meter
|
||||
const mixerId = `${mixer.mode ? 'M' : 'P'}${mixer.id}`;
|
||||
mixerIdRef.current = mixerId;
|
||||
const data = vuStore.getLevelSnapshot(mixerId);
|
||||
const level = data?.level ?? 0;
|
||||
const clipping = data?.clipping ?? false;
|
||||
const activeLights = Math.round(level * lightCount);
|
||||
|
||||
//console.debug("SessionTrackVU: VU registered for mixer", mixerId);
|
||||
// Direct DOM manipulation - no React involvement
|
||||
for (let i = 0; i < lightCount; i++) {
|
||||
const light = lightsRef.current[i];
|
||||
if (!light) continue;
|
||||
|
||||
const positionFromBottom = lightCount - 1 - i;
|
||||
const isActive = positionFromBottom < activeLights;
|
||||
|
||||
// Use className assignment for efficient class manipulation
|
||||
// (faster than classList.add/remove for multiple classes)
|
||||
if (isActive) {
|
||||
if (clipping || positionFromBottom >= Math.floor(lightCount * 0.75)) {
|
||||
light.className = 'vu-light vu-bg-danger';
|
||||
} else if (positionFromBottom >= Math.floor(lightCount * 0.5)) {
|
||||
light.className = 'vu-light vu-bg-warning';
|
||||
} else {
|
||||
light.className = 'vu-light vu-bg-success';
|
||||
}
|
||||
} else {
|
||||
light.className = 'vu-light vu-bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
rafIdRef.current = requestAnimationFrame(updateVisuals);
|
||||
};
|
||||
|
||||
rafIdRef.current = requestAnimationFrame(updateVisuals);
|
||||
|
||||
return () => {
|
||||
// Cleanup: reset VU state when component unmounts or mixer changes
|
||||
if (mixerIdRef.current) {
|
||||
updateVuState(mixerIdRef.current, 0, false);
|
||||
mixerIdRef.current = null;
|
||||
if (rafIdRef.current) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
}
|
||||
};
|
||||
}, [mixers, updateVuState]);
|
||||
}, [lightCount]);
|
||||
|
||||
// Use the React component for rendering
|
||||
// Render ONCE - never re-renders for VU updates
|
||||
return (
|
||||
<VuMeter
|
||||
mixerId={mixerIdRef.current}
|
||||
orientation={orientation}
|
||||
lightCount={lightCount}
|
||||
lightWidth={lightWidth}
|
||||
lightHeight={lightHeight}
|
||||
<div ref={containerRef} className="vu">
|
||||
<div className="d-flex flex-column" style={{ height: `${lightCount * (lightHeight + 1)}px` }}>
|
||||
{Array.from({ length: lightCount }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
ref={el => lightsRef.current[i] = el}
|
||||
className="vu-light vu-bg-secondary"
|
||||
style={{
|
||||
height: `${lightHeight}px`,
|
||||
width: '25px',
|
||||
marginTop: '1px',
|
||||
borderRadius: '2px',
|
||||
border: '1px solid #eee'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SessionTrackVU;
|
||||
|
|
|
|||
Loading…
Reference in New Issue