feat: allow user to use lasso to select multiple regions
This commit is contained in:
@@ -520,46 +520,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to select a region (clears previous selections)
|
// Helper function to select a region (clears previous selections)
|
||||||
const selectRegion = (
|
const applyRegionSelection = (orderedSelectionIds: string[]) => {
|
||||||
regionId: string,
|
|
||||||
options: RegionClickOptions = { shiftKey: false },
|
|
||||||
regionsToSearch?: RegionUI[]
|
|
||||||
) => {
|
|
||||||
// Find the region in the UI state (use provided regions or current state)
|
|
||||||
const regionsToUse = regionsToSearch || regions;
|
|
||||||
const region = regionsToUse.find(r => r.id === regionId);
|
|
||||||
if (!region) {
|
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
|
||||||
console.log(`Region not found in UI state: ${regionId}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the track that contains this region
|
|
||||||
const track = tracks.find(t => t.getId().toString() === region.trackId);
|
|
||||||
if (!track) {
|
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
|
||||||
console.log(`Track not found for region: ${regionId}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the region in the track's model
|
|
||||||
const coreRegion = track.getRegions().find(r => r.getId() === regionId);
|
|
||||||
|
|
||||||
if (!coreRegion) {
|
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
|
||||||
console.log(`Region not found in track model: ${regionId}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const core = KGCore.instance();
|
const core = KGCore.instance();
|
||||||
const orderedSelection = options.shiftKey
|
|
||||||
? (selectedRegionIds.includes(regionId)
|
|
||||||
? selectedRegionIds.filter(id => id !== regionId)
|
|
||||||
: [...selectedRegionIds, regionId])
|
|
||||||
: [regionId];
|
|
||||||
|
|
||||||
tracks.forEach(projectTrack => {
|
tracks.forEach(projectTrack => {
|
||||||
projectTrack.getRegions().forEach(projectRegion => projectRegion.deselect());
|
projectTrack.getRegions().forEach(projectRegion => projectRegion.deselect());
|
||||||
@@ -567,7 +529,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
clearAllSelections();
|
clearAllSelections();
|
||||||
|
|
||||||
const selectedRegions: KGRegion[] = orderedSelection
|
const selectedRegions: KGRegion[] = orderedSelectionIds
|
||||||
.map(selectedId => {
|
.map(selectedId => {
|
||||||
for (const projectTrack of tracks) {
|
for (const projectTrack of tracks) {
|
||||||
const selectedRegion = projectTrack.getRegions().find(r => r.getId() === selectedId);
|
const selectedRegion = projectTrack.getRegions().find(r => r.getId() === selectedId);
|
||||||
@@ -612,6 +574,67 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectRegion = (
|
||||||
|
regionId: string,
|
||||||
|
options: RegionClickOptions = { shiftKey: false },
|
||||||
|
regionsToSearch?: RegionUI[]
|
||||||
|
) => {
|
||||||
|
const regionsToUse = regionsToSearch || regions;
|
||||||
|
const region = regionsToUse.find(r => r.id === regionId);
|
||||||
|
if (!region) {
|
||||||
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
|
console.log(`Region not found in UI state: ${regionId}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const track = tracks.find(t => t.getId().toString() === region.trackId);
|
||||||
|
if (!track) {
|
||||||
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
|
console.log(`Track not found for region: ${regionId}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coreRegion = track.getRegions().find(r => r.getId() === regionId);
|
||||||
|
if (!coreRegion) {
|
||||||
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
|
console.log(`Region not found in track model: ${regionId}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedSelection = options.shiftKey
|
||||||
|
? (selectedRegionIds.includes(regionId)
|
||||||
|
? selectedRegionIds.filter(id => id !== regionId)
|
||||||
|
: [...selectedRegionIds, regionId])
|
||||||
|
: [regionId];
|
||||||
|
|
||||||
|
applyRegionSelection(orderedSelection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegionLassoSelection = (regionIds: string[], options: RegionClickOptions = { shiftKey: false }) => {
|
||||||
|
const orderedRegionIds = regionIds.filter(regionId => regions.some(region => region.id === regionId));
|
||||||
|
const orderedSelection = options.shiftKey
|
||||||
|
? orderedRegionIds.reduce<string[]>((nextSelection, regionId) => {
|
||||||
|
if (nextSelection.includes(regionId)) {
|
||||||
|
return nextSelection.filter(id => id !== regionId);
|
||||||
|
}
|
||||||
|
return [...nextSelection, regionId];
|
||||||
|
}, [...selectedRegionIds])
|
||||||
|
: orderedRegionIds;
|
||||||
|
|
||||||
|
applyRegionSelection(orderedSelection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmptyMainContentClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target !== e.currentTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRegionLassoSelection([], { shiftKey: false });
|
||||||
|
};
|
||||||
|
|
||||||
// Handle region single click: selection only (no piano roll opening)
|
// Handle region single click: selection only (no piano roll opening)
|
||||||
const handleRegionClick = (regionId: string, options: RegionClickOptions = { shiftKey: false }) => {
|
const handleRegionClick = (regionId: string, options: RegionClickOptions = { shiftKey: false }) => {
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
@@ -916,8 +939,12 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`main-content${showInstrumentSelection ? ' has-left-instrument' : ''}`} ref={mainContentRef}>
|
<div
|
||||||
<div className="main-content-wrapper">
|
className={`main-content${showInstrumentSelection ? ' has-left-instrument' : ''}`}
|
||||||
|
ref={mainContentRef}
|
||||||
|
onClick={handleEmptyMainContentClick}
|
||||||
|
>
|
||||||
|
<div className="main-content-wrapper" onClick={handleEmptyMainContentClick}>
|
||||||
{/* Top-left spacer */}
|
{/* Top-left spacer */}
|
||||||
<div className="top-left-spacer">
|
<div className="top-left-spacer">
|
||||||
<button className="add-track-btn" onClick={() => addTrack()}>+ MIDI</button>
|
<button className="add-track-btn" onClick={() => addTrack()}>+ MIDI</button>
|
||||||
@@ -940,7 +967,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="main-content-body">
|
<div className="main-content-body" onClick={handleEmptyMainContentClick}>
|
||||||
{/* Fixed left panel with track info */}
|
{/* Fixed left panel with track info */}
|
||||||
<TrackInfoPanel
|
<TrackInfoPanel
|
||||||
tracks={tracks}
|
tracks={tracks}
|
||||||
@@ -962,6 +989,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
onRegionCreated={handleRegionCreated}
|
onRegionCreated={handleRegionCreated}
|
||||||
onRegionUpdated={handleRegionUpdated}
|
onRegionUpdated={handleRegionUpdated}
|
||||||
onRegionClick={handleRegionClick}
|
onRegionClick={handleRegionClick}
|
||||||
|
onRegionLassoSelection={handleRegionLassoSelection}
|
||||||
onOpenPianoRoll={handleOpenPianoRoll}
|
onOpenPianoRoll={handleOpenPianoRoll}
|
||||||
onOpenSpectrogram={handleOpenSpectrogram}
|
onOpenSpectrogram={handleOpenSpectrogram}
|
||||||
showHybridButtonForAudio={showHybridButtonForAudio}
|
showHybridButtonForAudio={showHybridButtonForAudio}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
|
import TrackGridPanel from './TrackGridPanel';
|
||||||
|
import { createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data';
|
||||||
|
|
||||||
|
vi.mock('../../stores/projectStore', () => ({
|
||||||
|
useProjectStore: (selector?: (state: {
|
||||||
|
selectedRegionIds: string[],
|
||||||
|
refreshProjectState: () => void,
|
||||||
|
timeSignature: { numerator: number; denominator: number },
|
||||||
|
bpm: number,
|
||||||
|
}) => unknown) => {
|
||||||
|
const state = {
|
||||||
|
selectedRegionIds: [],
|
||||||
|
refreshProjectState: vi.fn(),
|
||||||
|
timeSignature: { numerator: 4, denominator: 4 },
|
||||||
|
bpm: 120,
|
||||||
|
};
|
||||||
|
return selector ? selector(state) : state;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../common', () => ({
|
||||||
|
Playhead: () => null,
|
||||||
|
FileImportModal: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('TrackGridPanel lasso selection', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
|
||||||
|
value: vi.fn(() => ({
|
||||||
|
clearRect: vi.fn(),
|
||||||
|
fillRect: vi.fn(),
|
||||||
|
beginPath: vi.fn(),
|
||||||
|
moveTo: vi.fn(),
|
||||||
|
lineTo: vi.fn(),
|
||||||
|
stroke: vi.fn(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
class ResizeObserverMock {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderPanel = () => {
|
||||||
|
const regionA = createMockMidiRegion({ id: 'region-a', trackId: '1', trackIndex: 0, startFromBeat: 0, length: 4 });
|
||||||
|
const regionB = createMockMidiRegion({ id: 'region-b', trackId: '2', trackIndex: 1, startFromBeat: 8, length: 4 });
|
||||||
|
const trackA = createMockMidiTrack({ id: 1, regions: [regionA] });
|
||||||
|
const trackB = createMockMidiTrack({ id: 2, regions: [regionB] });
|
||||||
|
trackA.setTrackIndex(0);
|
||||||
|
trackB.setTrackIndex(1);
|
||||||
|
|
||||||
|
const onRegionLassoSelection = vi.fn();
|
||||||
|
|
||||||
|
const view = render(
|
||||||
|
<TrackGridPanel
|
||||||
|
tracks={[trackA, trackB]}
|
||||||
|
regions={[
|
||||||
|
{ id: 'region-a', trackId: '1', trackIndex: 0, barNumber: 1, length: 1, name: 'Region A' },
|
||||||
|
{ id: 'region-b', trackId: '2', trackIndex: 1, barNumber: 3, length: 1, name: 'Region B' },
|
||||||
|
]}
|
||||||
|
maxBars={8}
|
||||||
|
timeSignature={{ numerator: 4, denominator: 4 }}
|
||||||
|
draggedTrackIndex={null}
|
||||||
|
dragOverTrackIndex={null}
|
||||||
|
selectedRegionId={null}
|
||||||
|
projectName="Test"
|
||||||
|
onRegionCreated={vi.fn()}
|
||||||
|
onRegionLassoSelection={onRegionLassoSelection}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridContainer = view.container.querySelector('.grid-container') as HTMLDivElement;
|
||||||
|
Object.defineProperty(gridContainer, 'clientWidth', { configurable: true, value: 320 });
|
||||||
|
vi.spyOn(gridContainer, 'getBoundingClientRect').mockReturnValue({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 320,
|
||||||
|
bottom: 240,
|
||||||
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...view, onRegionLassoSelection };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects intersecting regions across multiple track rows', () => {
|
||||||
|
const { container, onRegionLassoSelection } = renderPanel();
|
||||||
|
const firstTrackGrid = container.querySelector('[data-test-id="track-grid-1"]') as HTMLDivElement;
|
||||||
|
|
||||||
|
fireEvent.mouseDown(firstTrackGrid, { clientX: 10, clientY: 10, button: 0 });
|
||||||
|
fireEvent.mouseMove(document, { clientX: 130, clientY: 200 });
|
||||||
|
fireEvent.mouseUp(document, { clientX: 130, clientY: 200 });
|
||||||
|
|
||||||
|
expect(onRegionLassoSelection).toHaveBeenCalledWith(['region-a', 'region-b'], { shiftKey: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves the release-point region to the end of the lasso selection order', () => {
|
||||||
|
const { container, onRegionLassoSelection } = renderPanel();
|
||||||
|
const firstTrackGrid = container.querySelector('[data-test-id="track-grid-1"]') as HTMLDivElement;
|
||||||
|
|
||||||
|
fireEvent.mouseDown(firstTrackGrid, { clientX: 130, clientY: 200, button: 0 });
|
||||||
|
fireEvent.mouseMove(document, { clientX: 20, clientY: 20 });
|
||||||
|
fireEvent.mouseUp(document, { clientX: 20, clientY: 20 });
|
||||||
|
|
||||||
|
expect(onRegionLassoSelection).toHaveBeenCalledWith(['region-b', 'region-a'], { shiftKey: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears selection on a plain empty-space click', () => {
|
||||||
|
const { container, onRegionLassoSelection } = renderPanel();
|
||||||
|
const firstTrackGrid = container.querySelector('[data-test-id="track-grid-1"]') as HTMLDivElement;
|
||||||
|
|
||||||
|
fireEvent.mouseDown(firstTrackGrid, { clientX: 10, clientY: 10, button: 0 });
|
||||||
|
fireEvent.mouseUp(document, { clientX: 11, clientY: 11 });
|
||||||
|
|
||||||
|
expect(onRegionLassoSelection).toHaveBeenCalledWith([], { shiftKey: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { KGTrack, TrackType } from '../../core/track/KGTrack';
|
import { KGTrack, TrackType } from '../../core/track/KGTrack';
|
||||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
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 type { RegionClickOptions, RegionUI } from '../interfaces';
|
import type { RegionClickOptions, RegionUI } from '../interfaces';
|
||||||
import { DEBUG_MODE, 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';
|
||||||
import { CreateRegionCommand, ResizeRegionCommand, MoveRegionCommand, MoveMultipleRegionsCommand, ResizeMultipleRegionsCommand, ImportAudioCommand, ImportMidiClipCommand } from '../../core/commands';
|
import { CreateRegionCommand, ResizeRegionCommand, MoveRegionCommand, MoveMultipleRegionsCommand, ResizeMultipleRegionsCommand, ImportAudioCommand, ImportMidiClipCommand } from '../../core/commands';
|
||||||
@@ -30,6 +31,7 @@ interface TrackGridPanelProps {
|
|||||||
onRegionCreated: (trackIndex: number, region: RegionUI, midiRegion: KGMidiRegion) => void;
|
onRegionCreated: (trackIndex: number, region: RegionUI, midiRegion: KGMidiRegion) => void;
|
||||||
onRegionUpdated?: (regionId: string, updates: Partial<RegionUI>, expectedModelUpdates?: { startBeat: number, length: number }) => void;
|
onRegionUpdated?: (regionId: string, updates: Partial<RegionUI>, expectedModelUpdates?: { startBeat: number, length: number }) => void;
|
||||||
onRegionClick?: (regionId: string, options: RegionClickOptions) => void;
|
onRegionClick?: (regionId: string, options: RegionClickOptions) => void;
|
||||||
|
onRegionLassoSelection?: (regionIds: string[], options: RegionClickOptions) => void;
|
||||||
onOpenPianoRoll?: (regionId: string) => void;
|
onOpenPianoRoll?: (regionId: string) => void;
|
||||||
onOpenSpectrogram?: (regionId: string) => void;
|
onOpenSpectrogram?: (regionId: string) => void;
|
||||||
showHybridButtonForAudio?: boolean;
|
showHybridButtonForAudio?: boolean;
|
||||||
@@ -50,6 +52,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
onRegionCreated,
|
onRegionCreated,
|
||||||
onRegionUpdated,
|
onRegionUpdated,
|
||||||
onRegionClick,
|
onRegionClick,
|
||||||
|
onRegionLassoSelection,
|
||||||
onOpenPianoRoll,
|
onOpenPianoRoll,
|
||||||
onOpenSpectrogram,
|
onOpenSpectrogram,
|
||||||
showHybridButtonForAudio,
|
showHybridButtonForAudio,
|
||||||
@@ -62,6 +65,10 @@ 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 pendingAudioImportRef = useRef<{ barNumber: number; trackIndex: number } | null>(null);
|
const pendingAudioImportRef = useRef<{ barNumber: number; trackIndex: number } | null>(null);
|
||||||
|
const isLassoSelectingRef = useRef(false);
|
||||||
|
const isLassoShiftPressedRef = useRef(false);
|
||||||
|
const lassoBoxRef = useRef({ startX: 0, startY: 0, endX: 0, endY: 0 });
|
||||||
|
const [lassoRenderTick, setLassoRenderTick] = useState(0);
|
||||||
|
|
||||||
const getBulkSelectedRegionIds = (primaryRegionId: string) => (
|
const getBulkSelectedRegionIds = (primaryRegionId: string) => (
|
||||||
selectedRegionIds.length > 1 && selectedRegionIds.includes(primaryRegionId)
|
selectedRegionIds.length > 1 && selectedRegionIds.includes(primaryRegionId)
|
||||||
@@ -69,6 +76,111 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
: [primaryRegionId]
|
: [primaryRegionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const startLassoSelection = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
if (KGMainContentState.instance().getActiveTool() !== 'pointer') return;
|
||||||
|
if (isModifierKeyPressed(e)) return;
|
||||||
|
if (!(e.target instanceof HTMLElement)) return;
|
||||||
|
if (!e.target.closest('.track-grid')) return;
|
||||||
|
if (e.target.closest('.track-region')) return;
|
||||||
|
|
||||||
|
const rect = gridContainerRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
|
||||||
|
const startX = e.clientX - rect.left;
|
||||||
|
const startY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
isLassoSelectingRef.current = true;
|
||||||
|
isLassoShiftPressedRef.current = e.shiftKey;
|
||||||
|
lassoBoxRef.current = { startX, startY, endX: startX, endY: startY };
|
||||||
|
setLassoRenderTick(prev => prev + 1);
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleLassoMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleLassoMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLassoMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isLassoSelectingRef.current || !gridContainerRef.current) return;
|
||||||
|
|
||||||
|
const rect = gridContainerRef.current.getBoundingClientRect();
|
||||||
|
lassoBoxRef.current = {
|
||||||
|
...lassoBoxRef.current,
|
||||||
|
endX: e.clientX - rect.left,
|
||||||
|
endY: e.clientY - rect.top,
|
||||||
|
};
|
||||||
|
setLassoRenderTick(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLassoMouseUp = () => {
|
||||||
|
if (!isLassoSelectingRef.current || !gridContainerRef.current) return;
|
||||||
|
|
||||||
|
const { startX, startY, endX, endY } = lassoBoxRef.current;
|
||||||
|
const left = Math.min(startX, endX);
|
||||||
|
const top = Math.min(startY, endY);
|
||||||
|
const right = Math.max(startX, endX);
|
||||||
|
const bottom = Math.max(startY, endY);
|
||||||
|
const isClick = (right - left < PIANO_ROLL_CONSTANTS.DRAG_THRESHOLD)
|
||||||
|
&& (bottom - top < PIANO_ROLL_CONSTANTS.DRAG_THRESHOLD);
|
||||||
|
|
||||||
|
if (isClick) {
|
||||||
|
if (!isLassoShiftPressedRef.current) {
|
||||||
|
onRegionLassoSelection?.([], { shiftKey: false });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const containerWidth = gridContainerRef.current.clientWidth;
|
||||||
|
const barWidth = containerWidth > 0 ? containerWidth / maxBars : 0;
|
||||||
|
const releasePointX = endX;
|
||||||
|
const releasePointY = endY;
|
||||||
|
const intersectedRegions = barWidth > 0
|
||||||
|
? regions.filter(region => {
|
||||||
|
const regionLeft = (region.barNumber - 1) * barWidth;
|
||||||
|
const regionRight = regionLeft + (region.length * barWidth);
|
||||||
|
const regionTop = region.trackIndex * 120;
|
||||||
|
const regionBottom = regionTop + 120;
|
||||||
|
|
||||||
|
return !(
|
||||||
|
regionRight < left ||
|
||||||
|
regionLeft > right ||
|
||||||
|
regionBottom < top ||
|
||||||
|
regionTop > bottom
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const releaseRegionIndex = intersectedRegions.findIndex(region => {
|
||||||
|
const regionLeft = (region.barNumber - 1) * barWidth;
|
||||||
|
const regionRight = regionLeft + (region.length * barWidth);
|
||||||
|
const regionTop = region.trackIndex * 120;
|
||||||
|
const regionBottom = regionTop + 120;
|
||||||
|
|
||||||
|
return releasePointX >= regionLeft
|
||||||
|
&& releasePointX <= regionRight
|
||||||
|
&& releasePointY >= regionTop
|
||||||
|
&& releasePointY <= regionBottom;
|
||||||
|
});
|
||||||
|
|
||||||
|
const intersectedRegionIds = intersectedRegions.map(region => region.id);
|
||||||
|
if (releaseRegionIndex > -1) {
|
||||||
|
const [releaseRegionId] = intersectedRegionIds.splice(releaseRegionIndex, 1);
|
||||||
|
intersectedRegionIds.push(releaseRegionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRegionLassoSelection?.(intersectedRegionIds, { shiftKey: isLassoShiftPressedRef.current });
|
||||||
|
}
|
||||||
|
|
||||||
|
isLassoSelectingRef.current = false;
|
||||||
|
setLassoRenderTick(prev => prev + 1);
|
||||||
|
document.removeEventListener('mousemove', handleLassoMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleLassoMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleLassoMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleLassoMouseUp);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Utility function to create a region at a specific position
|
// Utility function to create a region at a specific position
|
||||||
const createRegionAtPosition = async (e: React.MouseEvent<HTMLDivElement>, trackIndex: number) => {
|
const createRegionAtPosition = async (e: React.MouseEvent<HTMLDivElement>, trackIndex: number) => {
|
||||||
// Get the grid container element
|
// Get the grid container element
|
||||||
@@ -738,7 +850,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid-container" ref={gridContainerRef}>
|
<div className="grid-container" ref={gridContainerRef} onMouseDownCapture={startLassoSelection}>
|
||||||
{/* Playhead */}
|
{/* Playhead */}
|
||||||
<Playhead context="main-grid" />
|
<Playhead context="main-grid" />
|
||||||
|
|
||||||
@@ -780,6 +892,11 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
title="Import Audio"
|
title="Import Audio"
|
||||||
description="Drag and drop your audio file here"
|
description="Drag and drop your audio file here"
|
||||||
/>
|
/>
|
||||||
|
<SelectionBox
|
||||||
|
key={lassoRenderTick}
|
||||||
|
isSelecting={isLassoSelectingRef.current}
|
||||||
|
selectionBox={lassoBoxRef.current}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user