fix: stabilize the preview canvas inside of the region during resizing
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,9 +780,16 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
<div
|
||||||
|
className="region-preview-content"
|
||||||
|
ref={previewContentRef}
|
||||||
|
style={previewContentStyle}
|
||||||
|
data-preview-content-active={previewContentStyle ? 'true' : 'false'}
|
||||||
|
>
|
||||||
<canvas ref={canvasRef} />
|
<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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user