fix: stabilize the preview canvas inside of the region during resizing

This commit is contained in:
Xiaohan-Tian
2026-05-19 17:57:21 -07:00
parent 4b5b5c8d76
commit b865064d65
7 changed files with 251 additions and 24 deletions
+5
View File
@@ -18,6 +18,11 @@ export interface RegionUI {
name: string; name: string;
} }
export interface RegionPreviewContentStyle {
left: string;
width: string;
}
export interface RegionClickOptions { export interface RegionClickOptions {
shiftKey: boolean; shiftKey: boolean;
} }
+14
View File
@@ -73,12 +73,26 @@
background-color: #87CEFA; /* Light blue */ background-color: #87CEFA; /* Light blue */
width: 100%; width: 100%;
position: relative; /* Allow overlayed controls */ position: relative; /* Allow overlayed controls */
overflow: hidden;
} }
.region-content.audio-region-content { .region-content.audio-region-content {
background-color: #90EE90; /* Light green for audio regions */ background-color: #90EE90; /* Light green for audio regions */
} }
.region-preview-content {
position: absolute;
inset: 0 auto 0 0;
width: 100%;
height: 100%;
}
.region-preview-content canvas {
display: block;
width: 100%;
height: 100%;
}
/* Audio region overrides */ /* Audio region overrides */
.track-region.audio-region { .track-region.audio-region {
background-color: #3a6b4a; background-color: #3a6b4a;
+31
View File
@@ -147,4 +147,35 @@ describe('RegionItem', () => {
expect(context.stroke).toHaveBeenCalled(); expect(context.stroke).toHaveBeenCalled();
rectSpy.mockRestore(); rectSpy.mockRestore();
}); });
it('applies preview content clipping styles when provided', () => {
const { container } = renderRegion({
previewContentStyle: {
left: '-40px',
width: '120px',
},
});
const previewContent = container.querySelector('.region-preview-content');
expect(previewContent).toBeTruthy();
expect(previewContent).toHaveAttribute('data-preview-content-active', 'true');
expect(previewContent).toHaveStyle({
left: '-40px',
width: '120px',
});
});
it('uses the default preview content wrapper sizing for normal regions', () => {
const { container } = renderRegion();
const previewContent = container.querySelector('.region-preview-content');
expect(previewContent).toBeTruthy();
expect(previewContent).toHaveAttribute('data-preview-content-active', 'false');
expect(previewContent).not.toHaveStyle({
left: '-40px',
width: '120px',
});
});
}); });
+37 -21
View File
@@ -9,6 +9,7 @@ import { KGAudioRegion } from '../../core/region/KGAudioRegion';
import { useProjectStore } from '../../stores/projectStore'; import { useProjectStore } from '../../stores/projectStore';
import { KGMainContentState } from '../../core/state/KGMainContentState'; import { KGMainContentState } from '../../core/state/KGMainContentState';
import type { AudioRecordingPeak } from '../../core/audio-interface/KGAudioRecorder'; import type { AudioRecordingPeak } from '../../core/audio-interface/KGAudioRecorder';
import type { RegionPreviewContentStyle } from '../interfaces';
const DRAG_START_THRESHOLD_PX = 4; const DRAG_START_THRESHOLD_PX = 4;
@@ -46,6 +47,7 @@ interface RegionItemProps {
previewWaveformPeaks?: AudioRecordingPeak[]; previewWaveformPeaks?: AudioRecordingPeak[];
isPreview?: boolean; isPreview?: boolean;
isAudioRegion?: boolean; isAudioRegion?: boolean;
previewContentStyle?: RegionPreviewContentStyle;
} }
const RegionItem: React.FC<RegionItemProps> = ({ const RegionItem: React.FC<RegionItemProps> = ({
@@ -73,6 +75,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
previewWaveformPeaks, previewWaveformPeaks,
isPreview = false, isPreview = false,
isAudioRegion = false, isAudioRegion = false,
previewContentStyle,
}) => { }) => {
// Get selection state and time signature from store // Get selection state and time signature from store
const { selectedRegionIds, timeSignature, bpm } = useProjectStore(); const { selectedRegionIds, timeSignature, bpm } = useProjectStore();
@@ -98,24 +101,26 @@ const RegionItem: React.FC<RegionItemProps> = ({
// Canvas ref for note visualization // Canvas ref for note visualization
const canvasRef = useRef<HTMLCanvasElement | null>(null); const canvasRef = useRef<HTMLCanvasElement | null>(null);
const regionContentRef = useRef<HTMLDivElement | null>(null); const previewContentRef = useRef<HTMLDivElement | null>(null);
// Function to render notes on canvas // Function to render notes on canvas
const renderNotesOnCanvas = () => { const renderNotesOnCanvas = () => {
if (!canvasRef.current || !regionContentRef.current || !midiRegion) return; if (!canvasRef.current || !previewContentRef.current || !midiRegion) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
// Get the current dimensions of the region content // Get the current dimensions of the region content
const contentRect = regionContentRef.current.getBoundingClientRect(); const contentRect = previewContentRef.current.getBoundingClientRect();
const width = contentRect.width; const width = Math.max(1, Math.round(contentRect.width));
const height = contentRect.height; const height = Math.max(1, Math.round(contentRect.height));
// Set canvas size to match the region content // Set canvas size to match the region content
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
// Clear the canvas // Clear the canvas
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
@@ -242,18 +247,20 @@ const RegionItem: React.FC<RegionItemProps> = ({
// Function to render audio waveform on canvas // Function to render audio waveform on canvas
const renderWaveformOnCanvas = () => { const renderWaveformOnCanvas = () => {
if (!canvasRef.current || !regionContentRef.current || !audioBuffer) return; if (!canvasRef.current || !previewContentRef.current || !audioBuffer) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
const contentRect = regionContentRef.current.getBoundingClientRect(); const contentRect = previewContentRef.current.getBoundingClientRect();
const width = contentRect.width; const width = Math.max(1, Math.round(contentRect.width));
const height = contentRect.height; const height = Math.max(1, Math.round(contentRect.height));
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
@@ -311,18 +318,20 @@ const RegionItem: React.FC<RegionItemProps> = ({
}; };
const renderPreviewWaveformOnCanvas = () => { const renderPreviewWaveformOnCanvas = () => {
if (!canvasRef.current || !regionContentRef.current || !previewWaveformPeaks || previewWaveformPeaks.length === 0) return; if (!canvasRef.current || !previewContentRef.current || !previewWaveformPeaks || previewWaveformPeaks.length === 0) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
const contentRect = regionContentRef.current.getBoundingClientRect(); const contentRect = previewContentRef.current.getBoundingClientRect();
const width = contentRect.width; const width = Math.max(1, Math.round(contentRect.width));
const height = contentRect.height; const height = Math.max(1, Math.round(contentRect.height));
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
const centerY = height / 2; const centerY = height / 2;
@@ -374,11 +383,11 @@ const RegionItem: React.FC<RegionItemProps> = ({
} else { } else {
renderNotesOnCanvas(); renderNotesOnCanvas();
} }
}, [midiRegion, audioRegion, audioBuffer, previewWaveformPeaks, timeSignature, bpm, id, noteUpdateTrigger, barNumber, length]); }, [midiRegion, audioRegion, audioBuffer, previewWaveformPeaks, timeSignature, bpm, id, noteUpdateTrigger, barNumber, length, previewContentStyle?.width]);
// Re-render canvas when region content size changes // Re-render canvas when region content size changes
useEffect(() => { useEffect(() => {
if (!regionContentRef.current) return; if (!previewContentRef.current) return;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (previewWaveformPeaks && previewWaveformPeaks.length > 0) { if (previewWaveformPeaks && previewWaveformPeaks.length > 0) {
@@ -390,14 +399,14 @@ const RegionItem: React.FC<RegionItemProps> = ({
} }
}); });
resizeObserver.observe(regionContentRef.current); resizeObserver.observe(previewContentRef.current);
return () => { return () => {
if (regionContentRef.current) { if (previewContentRef.current) {
resizeObserver.unobserve(regionContentRef.current); resizeObserver.unobserve(previewContentRef.current);
} }
}; };
}, [midiRegion, audioRegion, audioBuffer, previewWaveformPeaks, timeSignature, bpm]); }, [midiRegion, audioRegion, audioBuffer, previewWaveformPeaks, timeSignature, bpm, previewContentStyle?.width]);
// Handle mouse movement to detect edge proximity // Handle mouse movement to detect edge proximity
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => { const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
@@ -668,7 +677,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
<div className="region-header"> <div className="region-header">
{name} {name}
</div> </div>
<div className={`region-content${(audioRegion || isAudioRegion) ? ' audio-region-content' : ''}`} ref={regionContentRef}> <div className={`region-content${(audioRegion || isAudioRegion) ? ' audio-region-content' : ''}`}>
{!isPreview && <div className="region-left-buttons"> {!isPreview && <div className="region-left-buttons">
{!audioRegion && ( {!audioRegion && (
<button <button
@@ -771,7 +780,14 @@ const RegionItem: React.FC<RegionItemProps> = ({
)} )}
</div> </div>
</div>} </div>}
<canvas ref={canvasRef} /> <div
className="region-preview-content"
ref={previewContentRef}
style={previewContentStyle}
data-preview-content-active={previewContentStyle ? 'true' : 'false'}
>
<canvas ref={canvasRef} />
</div>
</div> </div>
</div> </div>
); );
@@ -5,6 +5,7 @@ import TrackGridItem from './TrackGridItem';
import { KGAudioTrack } from '../../core/track/KGAudioTrack'; import { KGAudioTrack } from '../../core/track/KGAudioTrack';
import { KGMainContentState } from '../../core/state/KGMainContentState'; import { KGMainContentState } from '../../core/state/KGMainContentState';
import { createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data'; import { createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data';
import type { RegionPreviewContentStyle } from '../interfaces';
const storeState = { const storeState = {
selectedRegionIds: [] as string[], selectedRegionIds: [] as string[],
@@ -56,6 +57,7 @@ vi.mock('../../core/audio-interface/KGAudioInterface', () => ({
describe('TrackGridItem preview behavior', () => { describe('TrackGridItem preview behavior', () => {
const getRegionItem = (regionId: string) => regionItemProps.get(regionId) as { const getRegionItem = (regionId: string) => regionItemProps.get(regionId) as {
style: React.CSSProperties; style: React.CSSProperties;
previewContentStyle?: RegionPreviewContentStyle;
onResizeStart?: (regionId: string, resizeAction: 'start' | 'end', initialX: number) => void; onResizeStart?: (regionId: string, resizeAction: 'start' | 'end', initialX: number) => void;
onResize?: (regionId: string, resizeAction: 'start' | 'end', deltaX: number) => void; onResize?: (regionId: string, resizeAction: 'start' | 'end', deltaX: number) => void;
onResizeEnd?: (regionId: string, resizeAction: 'start' | 'end') => void; onResizeEnd?: (regionId: string, resizeAction: 'start' | 'end') => void;
@@ -145,6 +147,7 @@ describe('TrackGridItem preview behavior', () => {
const SharedPreviewHarness = () => { const SharedPreviewHarness = () => {
const [previewRegionStyles, setPreviewRegionStyles] = useState<Record<string, React.CSSProperties>>({}); const [previewRegionStyles, setPreviewRegionStyles] = useState<Record<string, React.CSSProperties>>({});
const [previewRegionContentStyles, setPreviewRegionContentStyles] = useState<Record<string, RegionPreviewContentStyle>>({});
return ( return (
<> <>
@@ -153,6 +156,8 @@ describe('TrackGridItem preview behavior', () => {
index={0} index={0}
previewRegionStyles={previewRegionStyles} previewRegionStyles={previewRegionStyles}
setPreviewRegionStyles={setPreviewRegionStyles} setPreviewRegionStyles={setPreviewRegionStyles}
previewRegionContentStyles={previewRegionContentStyles}
setPreviewRegionContentStyles={setPreviewRegionContentStyles}
{...baseProps} {...baseProps}
/> />
<TrackGridItem <TrackGridItem
@@ -160,6 +165,8 @@ describe('TrackGridItem preview behavior', () => {
index={1} index={1}
previewRegionStyles={previewRegionStyles} previewRegionStyles={previewRegionStyles}
setPreviewRegionStyles={setPreviewRegionStyles} setPreviewRegionStyles={setPreviewRegionStyles}
previewRegionContentStyles={previewRegionContentStyles}
setPreviewRegionContentStyles={setPreviewRegionContentStyles}
{...baseProps} {...baseProps}
/> />
</> </>
@@ -205,11 +212,19 @@ describe('TrackGridItem preview behavior', () => {
width: '140px', width: '140px',
position: 'absolute', position: 'absolute',
}); });
expect(getRegionItem('region-a').previewContentStyle).toEqual({
left: '0px',
width: '100px',
});
expect(getRegionItem('region-b').style).toEqual({ expect(getRegionItem('region-b').style).toEqual({
left: '200px', left: '200px',
width: '240px', width: '240px',
position: 'absolute', position: 'absolute',
}); });
expect(getRegionItem('region-b').previewContentStyle).toEqual({
left: '0px',
width: '200px',
});
}); });
it('previews start resize for all selected regions across track rows', () => { it('previews start resize for all selected regions across track rows', () => {
@@ -231,11 +246,87 @@ describe('TrackGridItem preview behavior', () => {
width: '160px', width: '160px',
position: 'absolute', position: 'absolute',
}); });
expect(getRegionItem('region-a').previewContentStyle).toEqual({
left: '-40px',
width: '200px',
});
expect(getRegionItem('region-b').style).toEqual({ expect(getRegionItem('region-b').style).toEqual({
left: '340px', left: '340px',
width: '260px', width: '260px',
position: 'absolute', position: 'absolute',
}); });
expect(getRegionItem('region-b').previewContentStyle).toEqual({
left: '-40px',
width: '300px',
});
});
it('keeps preview content fixed while shrinking from the end', () => {
renderSharedPreviewHarness(
['region-a', 'region-b'],
[
{ id: 'region-a', trackId: '1', trackIndex: 0, barNumber: 1, length: 2, name: 'Region A' },
{ id: 'region-b', trackId: '2', trackIndex: 1, barNumber: 3, length: 3, name: 'Region B' },
],
);
act(() => {
getRegionItem('region-a').onResizeStart?.('region-a', 'end', 0);
getRegionItem('region-a').onResize?.('region-a', 'end', -40);
});
expect(getRegionItem('region-a').style).toEqual({
left: '0px',
width: '160px',
position: 'absolute',
});
expect(getRegionItem('region-a').previewContentStyle).toEqual({
left: '0px',
width: '200px',
});
expect(getRegionItem('region-b').style).toEqual({
left: '200px',
width: '260px',
position: 'absolute',
});
expect(getRegionItem('region-b').previewContentStyle).toEqual({
left: '0px',
width: '300px',
});
});
it('shifts preview content right when extending from the start', () => {
renderSharedPreviewHarness(
['region-a', 'region-b'],
[
{ id: 'region-a', trackId: '1', trackIndex: 0, barNumber: 2, length: 2, name: 'Region A' },
{ id: 'region-b', trackId: '2', trackIndex: 1, barNumber: 4, length: 3, name: 'Region B' },
],
);
act(() => {
getRegionItem('region-a').onResizeStart?.('region-a', 'start', 0);
getRegionItem('region-a').onResize?.('region-a', 'start', -40);
});
expect(getRegionItem('region-a').style).toEqual({
left: '60px',
width: '240px',
position: 'absolute',
});
expect(getRegionItem('region-a').previewContentStyle).toEqual({
left: '40px',
width: '200px',
});
expect(getRegionItem('region-b').style).toEqual({
left: '260px',
width: '340px',
position: 'absolute',
});
expect(getRegionItem('region-b').previewContentStyle).toEqual({
left: '40px',
width: '300px',
});
}); });
it('previews drag movement for all selected regions across track rows', () => { it('previews drag movement for all selected regions across track rows', () => {
@@ -320,11 +411,13 @@ describe('TrackGridItem preview behavior', () => {
width: '100px', width: '100px',
position: 'absolute', position: 'absolute',
}); });
expect(getRegionItem('region-a').previewContentStyle).toBeUndefined();
expect(getRegionItem('region-b').style).toEqual({ expect(getRegionItem('region-b').style).toEqual({
left: '200px', left: '200px',
width: '200px', width: '200px',
position: 'absolute', position: 'absolute',
}); });
expect(getRegionItem('region-b').previewContentStyle).toBeUndefined();
expect(onRegionResizeEnd).toHaveBeenCalledWith('region-a', 1, 1); expect(onRegionResizeEnd).toHaveBeenCalledWith('region-a', 1, 1);
act(() => { act(() => {
@@ -338,11 +431,13 @@ describe('TrackGridItem preview behavior', () => {
width: '100px', width: '100px',
position: 'absolute', position: 'absolute',
}); });
expect(getRegionItem('region-a').previewContentStyle).toBeUndefined();
expect(getRegionItem('region-b').style).toEqual({ expect(getRegionItem('region-b').style).toEqual({
left: '200px', left: '200px',
width: '200px', width: '200px',
position: 'absolute', position: 'absolute',
}); });
expect(getRegionItem('region-b').previewContentStyle).toBeUndefined();
expect(onRegionDragEnd).toHaveBeenCalledWith('region-a', 2, 0); expect(onRegionDragEnd).toHaveBeenCalledWith('region-a', 2, 0);
}); });
}); });
+65 -2
View File
@@ -5,7 +5,7 @@ import { KGAudioRegion } from '../../core/region/KGAudioRegion';
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface'; import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
import RegionItem from './RegionItem'; import RegionItem from './RegionItem';
import TrackAutomationLane from './TrackAutomationLane'; import TrackAutomationLane from './TrackAutomationLane';
import type { RegionClickOptions, RegionUI, ResizeAction } from '../interfaces'; import type { RegionClickOptions, RegionPreviewContentStyle, RegionUI, ResizeAction } from '../interfaces';
import { REGION_CONSTANTS, DEBUG_MODE } from '../../constants'; import { REGION_CONSTANTS, DEBUG_MODE } from '../../constants';
import { KGMainContentState } from '../../core/state/KGMainContentState'; import { KGMainContentState } from '../../core/state/KGMainContentState';
import { isModifierKeyPressed } from '../../util/osUtil'; import { isModifierKeyPressed } from '../../util/osUtil';
@@ -17,6 +17,7 @@ interface RegionResizePreviewBaseline {
originalLength: number; originalLength: number;
originalLeft: number; originalLeft: number;
originalWidth: number; originalWidth: number;
originalContentWidth: number;
} }
interface RegionDragPreviewBaseline { interface RegionDragPreviewBaseline {
@@ -53,6 +54,8 @@ interface TrackGridItemProps {
onKGOneClipDrop?: (e: React.DragEvent<HTMLDivElement>, trackIndex: number) => void; onKGOneClipDrop?: (e: React.DragEvent<HTMLDivElement>, trackIndex: number) => void;
previewRegionStyles?: Record<string, React.CSSProperties>; previewRegionStyles?: Record<string, React.CSSProperties>;
setPreviewRegionStyles?: React.Dispatch<React.SetStateAction<Record<string, React.CSSProperties>>>; setPreviewRegionStyles?: React.Dispatch<React.SetStateAction<Record<string, React.CSSProperties>>>;
previewRegionContentStyles?: Record<string, RegionPreviewContentStyle>;
setPreviewRegionContentStyles?: React.Dispatch<React.SetStateAction<Record<string, RegionPreviewContentStyle>>>;
} }
const TrackGridItem: React.FC<TrackGridItemProps> = ({ const TrackGridItem: React.FC<TrackGridItemProps> = ({
@@ -81,6 +84,8 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
onKGOneClipDrop, onKGOneClipDrop,
previewRegionStyles, previewRegionStyles,
setPreviewRegionStyles, setPreviewRegionStyles,
previewRegionContentStyles,
setPreviewRegionContentStyles,
}) => { }) => {
const selectedRegionIds = useProjectStore(state => state.selectedRegionIds); const selectedRegionIds = useProjectStore(state => state.selectedRegionIds);
const activeTrackAutomationTrackId = useProjectStore(state => state.activeTrackAutomationTrackId); const activeTrackAutomationTrackId = useProjectStore(state => state.activeTrackAutomationTrackId);
@@ -97,6 +102,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
const [resizingRegion, setResizingRegion] = useState<string | null>(null); const [resizingRegion, setResizingRegion] = useState<string | null>(null);
const [draggingRegion, setDraggingRegion] = useState<string | null>(null); const [draggingRegion, setDraggingRegion] = useState<string | null>(null);
const [localTempRegionStyles, setLocalTempRegionStyles] = useState<Record<string, React.CSSProperties>>({}); const [localTempRegionStyles, setLocalTempRegionStyles] = useState<Record<string, React.CSSProperties>>({});
const [localPreviewRegionContentStyles, setLocalPreviewRegionContentStyles] = useState<Record<string, RegionPreviewContentStyle>>({});
const [isModifierPressed, setIsModifierPressed] = useState(false); const [isModifierPressed, setIsModifierPressed] = useState(false);
// Refs for resize operations // Refs for resize operations
@@ -120,6 +126,8 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
const isBulkRegionEdit = (regionId: string) => selectedRegionIds.length > 1 && selectedRegionIds.includes(regionId); const isBulkRegionEdit = (regionId: string) => selectedRegionIds.length > 1 && selectedRegionIds.includes(regionId);
const tempRegionStyles = previewRegionStyles ?? localTempRegionStyles; const tempRegionStyles = previewRegionStyles ?? localTempRegionStyles;
const setTempRegionStyles = setPreviewRegionStyles ?? setLocalTempRegionStyles; const setTempRegionStyles = setPreviewRegionStyles ?? setLocalTempRegionStyles;
const tempPreviewRegionContentStyles = previewRegionContentStyles ?? localPreviewRegionContentStyles;
const setTempPreviewRegionContentStyles = setPreviewRegionContentStyles ?? setLocalPreviewRegionContentStyles;
const getPreviewRegionIds = (regionId: string) => ( const getPreviewRegionIds = (regionId: string) => (
selectedRegionIds.length > 1 && selectedRegionIds.includes(regionId) selectedRegionIds.length > 1 && selectedRegionIds.includes(regionId)
@@ -142,6 +150,34 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
}); });
}; };
const clearTempPreviewRegionContentStyles = (regionIds?: string[]) => {
if (!regionIds || regionIds.length === 0) {
setTempPreviewRegionContentStyles({});
return;
}
setTempPreviewRegionContentStyles(prev => {
const updated = { ...prev };
regionIds.forEach(id => {
delete updated[id];
});
return updated;
});
};
const getMeasuredRegionContentWidth = (regionId: string, fallbackWidth: number) => {
const regionElement = Array.from(document.querySelectorAll<HTMLElement>('[data-region-id]'))
.find(element => element.getAttribute('data-region-id') === regionId);
const regionContentElement = regionElement?.querySelector<HTMLElement>('.region-content');
const measuredWidth = regionContentElement?.getBoundingClientRect().width;
if (!measuredWidth || Number.isNaN(measuredWidth)) {
return fallbackWidth;
}
return measuredWidth;
};
// Update container width when the grid container changes size // Update container width when the grid container changes size
useEffect(() => { useEffect(() => {
if (!gridContainerRef.current) return; if (!gridContainerRef.current) return;
@@ -253,6 +289,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
originalLength: candidate.length, originalLength: candidate.length,
originalLeft: (candidate.barNumber - 1) * barWidth, originalLeft: (candidate.barNumber - 1) * barWidth,
originalWidth: candidate.length * barWidth, originalWidth: candidate.length * barWidth,
originalContentWidth: getMeasuredRegionContentWidth(candidate.id, candidate.length * barWidth),
})); }));
resizePreviewRegionIdsRef.current = resizePreviewBaselinesRef.current.map(baseline => baseline.regionId); resizePreviewRegionIdsRef.current = resizePreviewBaselinesRef.current.map(baseline => baseline.regionId);
@@ -267,6 +304,17 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
}, },
])), ])),
})); }));
setTempPreviewRegionContentStyles(prev => ({
...prev,
...Object.fromEntries(resizePreviewBaselinesRef.current.map(baseline => [
baseline.regionId,
{
left: '0px',
width: `${baseline.originalContentWidth}px`,
},
])),
}));
}; };
// Handle region resize // Handle region resize
@@ -324,6 +372,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
originalLength: region.length, originalLength: region.length,
originalLeft, originalLeft,
originalWidth, originalWidth,
originalContentWidth: getMeasuredRegionContentWidth(regionId, originalWidth),
}]; }];
setTempRegionStyles(prev => ({ setTempRegionStyles(prev => ({
@@ -338,6 +387,17 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
])), ])),
})); }));
setTempPreviewRegionContentStyles(prev => ({
...prev,
...Object.fromEntries(previewBaselines.map(baseline => [
baseline.regionId,
{
left: `${resizeAction === 'start' ? -(newLeft - originalLeft) : 0}px`,
width: `${baseline.originalContentWidth}px`,
},
])),
}));
// Notify parent about resize // Notify parent about resize
if (onRegionResize) { if (onRegionResize) {
onRegionResize(regionId, newBarNumber, newLength); onRegionResize(regionId, newBarNumber, newLength);
@@ -401,6 +461,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
// Clear resizing state // Clear resizing state
setResizingRegion(null); setResizingRegion(null);
clearTempRegionStyles(resizePreviewRegionIdsRef.current); clearTempRegionStyles(resizePreviewRegionIdsRef.current);
clearTempPreviewRegionContentStyles(resizePreviewRegionIdsRef.current);
currentResizeWidth.current = null; currentResizeWidth.current = null;
currentResizeLeft.current = null; currentResizeLeft.current = null;
currentResizeRegion.current = null; currentResizeRegion.current = null;
@@ -602,6 +663,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
// Clear dragging state // Clear dragging state
setDraggingRegion(null); setDraggingRegion(null);
clearTempRegionStyles(dragPreviewRegionIdsRef.current); clearTempRegionStyles(dragPreviewRegionIdsRef.current);
clearTempPreviewRegionContentStyles(dragPreviewRegionIdsRef.current);
currentDragLeft.current = null; currentDragLeft.current = null;
currentDragTop.current = null; currentDragTop.current = null;
currentDragRegion.current = null; currentDragRegion.current = null;
@@ -665,7 +727,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
}} }}
onClick={(e) => { onClick={(e) => {
if (!isAutomationActive) { if (!isAutomationActive) {
onClick && onClick(e, index); onClick?.(e, index);
} }
}} }}
ref={trackElementRef} ref={trackElementRef}
@@ -733,6 +795,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
midiRegion={midiRegion} midiRegion={midiRegion}
audioRegion={audioRegion} audioRegion={audioRegion}
audioBuffer={audioBuffer} audioBuffer={audioBuffer}
previewContentStyle={tempPreviewRegionContentStyles[region.id]}
/> />
); );
})} })}
+4 -1
View File
@@ -4,7 +4,7 @@ import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import TrackGridItem from './TrackGridItem'; import TrackGridItem from './TrackGridItem';
import { Playhead, FileImportModal } from '../common'; import { Playhead, FileImportModal } from '../common';
import SelectionBox from '../piano-roll/SelectionBox'; import SelectionBox from '../piano-roll/SelectionBox';
import type { RegionClickOptions, RegionUI } from '../interfaces'; import type { RegionClickOptions, RegionPreviewContentStyle, RegionUI } from '../interfaces';
import { DEBUG_MODE, PIANO_ROLL_CONSTANTS, REGION_CONSTANTS } from '../../constants'; import { DEBUG_MODE, PIANO_ROLL_CONSTANTS, REGION_CONSTANTS } from '../../constants';
import { KGMainContentState } from '../../core/state/KGMainContentState'; import { KGMainContentState } from '../../core/state/KGMainContentState';
import { isModifierKeyPressed } from '../../util/osUtil'; import { isModifierKeyPressed } from '../../util/osUtil';
@@ -67,6 +67,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
const gridContainerRef = useRef<HTMLDivElement>(null); const gridContainerRef = useRef<HTMLDivElement>(null);
const [showAudioImportModal, setShowAudioImportModal] = useState(false); const [showAudioImportModal, setShowAudioImportModal] = useState(false);
const [previewRegionStyles, setPreviewRegionStyles] = useState<Record<string, React.CSSProperties>>({}); const [previewRegionStyles, setPreviewRegionStyles] = useState<Record<string, React.CSSProperties>>({});
const [previewRegionContentStyles, setPreviewRegionContentStyles] = useState<Record<string, RegionPreviewContentStyle>>({});
const pendingAudioImportRef = useRef<{ barNumber: number; trackIndex: number } | null>(null); const pendingAudioImportRef = useRef<{ barNumber: number; trackIndex: number } | null>(null);
const isLassoSelectingRef = useRef(false); const isLassoSelectingRef = useRef(false);
const isLassoShiftPressedRef = useRef(false); const isLassoShiftPressedRef = useRef(false);
@@ -910,6 +911,8 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
onKGOneClipDrop={handleExternalDrop} onKGOneClipDrop={handleExternalDrop}
previewRegionStyles={previewRegionStyles} previewRegionStyles={previewRegionStyles}
setPreviewRegionStyles={setPreviewRegionStyles} setPreviewRegionStyles={setPreviewRegionStyles}
previewRegionContentStyles={previewRegionContentStyles}
setPreviewRegionContentStyles={setPreviewRegionContentStyles}
/> />
))} ))}