feat: add snap-to-grid toggle for track grid regions and playhead
This commit is contained in:
@@ -13,6 +13,7 @@ import type { RegionUI } from './interfaces';
|
||||
import { DEBUG_MODE, BAR_NUMBERS_CONSTANTS } from '../constants';
|
||||
import { useRegionOperations } from '../hooks/useRegionOperations';
|
||||
import { regionDeleteManager } from '../util/regionDeleteUtil';
|
||||
import { KGMainContentState } from '../core/state/KGMainContentState';
|
||||
import { ChangeLoopSettingsCommand } from '../core/commands';
|
||||
|
||||
interface MainContentProps {
|
||||
@@ -135,7 +136,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
if (region instanceof KGMidiRegion || region instanceof KGAudioRegion) {
|
||||
// Calculate bar number and length from beats
|
||||
const beatsPerBar = timeSignature.numerator;
|
||||
const barNumber = Math.floor(region.getStartFromBeat() / beatsPerBar) + 1;
|
||||
const barNumber = (region.getStartFromBeat() / beatsPerBar) + 1;
|
||||
const length = region.getLength() / beatsPerBar;
|
||||
|
||||
// Create a RegionUI object
|
||||
@@ -517,8 +518,9 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--track-grid-bar-width')
|
||||
) || 40;
|
||||
|
||||
// Find the closest bar start (using Math.round for nearest bar)
|
||||
const barIndex = Math.round(relativeX / barWidth);
|
||||
// Find the closest bar start; honor snapping toggle
|
||||
const snap = KGMainContentState.instance().isSnappingEnabled();
|
||||
const barIndex = snap ? Math.round(relativeX / barWidth) : relativeX / barWidth;
|
||||
|
||||
// Ensure we don't go below 0
|
||||
const clampedBarIndex = Math.max(0, barIndex);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
FaUndo, FaRedo, FaMousePointer, FaStepBackward,
|
||||
FaPlay, FaPause, FaComments, FaSync,
|
||||
FaFolderOpen, FaSave, FaDownload, FaUpload, FaPlus,
|
||||
FaCog
|
||||
FaCog, FaMagnet
|
||||
} from 'react-icons/fa';
|
||||
import { KGProject, type KeySignature } from '../core/KGProject';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
@@ -48,6 +48,7 @@ const Toolbar: React.FC = () => {
|
||||
|
||||
// State for main content tools
|
||||
const [activeMainTool, setActiveMainTool] = React.useState<'pointer' | 'pencil'>('pointer');
|
||||
const [isSnapping, setIsSnapping] = React.useState(true);
|
||||
|
||||
// State for key signature dropdown
|
||||
const [showKeySignatureDropdown, setShowKeySignatureDropdown] = React.useState(false);
|
||||
@@ -569,6 +570,16 @@ const Toolbar: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle snapping toggle
|
||||
const handleSnappingToggle = () => {
|
||||
const newValue = !isSnapping;
|
||||
setIsSnapping(newValue);
|
||||
KGMainContentState.instance().setSnapping(newValue);
|
||||
if (DEBUG_MODE.TOOLBAR) {
|
||||
console.log(`Snapping ${newValue ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle copy button click
|
||||
const handleCopyClick = () => {
|
||||
if (DEBUG_MODE.TOOLBAR) {
|
||||
@@ -774,13 +785,20 @@ const Toolbar: React.FC = () => {
|
||||
>
|
||||
<FaMousePointer />
|
||||
</button>
|
||||
<button
|
||||
title="Pencil"
|
||||
<button
|
||||
title="Pencil"
|
||||
className={`tool-button ${activeMainTool === 'pencil' ? 'active' : ''}`}
|
||||
onClick={() => handleMainToolSelect('pencil')}
|
||||
>
|
||||
<FaPencil />
|
||||
</button>
|
||||
<button
|
||||
title="Snap to Grid"
|
||||
className={`tool-button ${isSnapping ? 'active' : ''}`}
|
||||
onClick={handleSnappingToggle}
|
||||
>
|
||||
<FaMagnet />
|
||||
</button>
|
||||
<div className="toolbar-separator"></div>
|
||||
<button title="Copy" onClick={handleCopyClick}><FaCopy /></button>
|
||||
<button title="Paste" onClick={handlePasteClick}><FaPaste /></button>
|
||||
|
||||
@@ -307,7 +307,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
||||
} else {
|
||||
renderNotesOnCanvas();
|
||||
}
|
||||
}, [midiRegion, audioRegion, audioBuffer, timeSignature, bpm, id, noteUpdateTrigger]);
|
||||
}, [midiRegion, audioRegion, audioBuffer, timeSignature, bpm, id, noteUpdateTrigger, barNumber, length]);
|
||||
|
||||
// Re-render canvas when region content size changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -268,20 +268,23 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
||||
|
||||
// If the mouse was moved and we have current values, calculate the new values
|
||||
if (mouseMoved.current && currentResizeWidth.current !== null && currentResizeLeft.current !== null) {
|
||||
const snap = KGMainContentState.instance().isSnappingEnabled();
|
||||
|
||||
if (resizeAction === 'end') {
|
||||
// End resize: round length to nearest bar
|
||||
newLength = Math.max(REGION_CONSTANTS.MIN_REGION_LENGTH, Math.round(currentResizeWidth.current / barWidth));
|
||||
// End resize: snap length to nearest bar, or use raw value
|
||||
const rawLength = currentResizeWidth.current / barWidth;
|
||||
newLength = Math.max(REGION_CONSTANTS.MIN_REGION_LENGTH, snap ? Math.round(rawLength) : rawLength);
|
||||
} else if (resizeAction === 'start') {
|
||||
// Start resize: round bar number and adjust length accordingly
|
||||
// Start resize: snap bar number, or use raw value
|
||||
const rawBarNumber = currentResizeLeft.current / barWidth + 1;
|
||||
newBarNumber = Math.max(1, Math.round(rawBarNumber));
|
||||
|
||||
newBarNumber = Math.max(1, snap ? Math.round(rawBarNumber) : rawBarNumber);
|
||||
|
||||
// Calculate the difference from the initial position
|
||||
const barDiff = initialBarNumberRef.current! - newBarNumber;
|
||||
|
||||
|
||||
// Adjust length to maintain the end position
|
||||
newLength = initialLengthRef.current! + barDiff;
|
||||
|
||||
|
||||
// Ensure minimum length
|
||||
if (newLength < REGION_CONSTANTS.MIN_REGION_LENGTH) {
|
||||
newLength = REGION_CONSTANTS.MIN_REGION_LENGTH;
|
||||
@@ -434,9 +437,10 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
||||
|
||||
// If the mouse was moved, calculate the final position
|
||||
if (mouseMoved.current && currentDragLeft.current !== null && currentDragTop.current !== null) {
|
||||
// Calculate the new bar number and round to nearest integer
|
||||
// Calculate the new bar number; snap to nearest integer when snapping is on
|
||||
const snap = KGMainContentState.instance().isSnappingEnabled();
|
||||
const rawBarNumber = (currentDragLeft.current / barWidth) + 1;
|
||||
finalBarNumber = Math.max(1, Math.round(rawBarNumber));
|
||||
finalBarNumber = Math.max(1, snap ? Math.round(rawBarNumber) : rawBarNumber);
|
||||
|
||||
// Calculate the closest track based on vertical position
|
||||
if (allTracks && allTracks.length > 0 && gridContainerRef.current) {
|
||||
|
||||
@@ -193,6 +193,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
||||
const secondsPerBeat = 60 / bpm;
|
||||
const clipOffset = coreRegion.getClipStartOffsetSeconds();
|
||||
const audioDuration = coreRegion.getAudioDurationSeconds();
|
||||
const snap = KGMainContentState.instance().isSnappingEnabled();
|
||||
|
||||
// Left edge changed — calculate new clip offset
|
||||
if (clampedBarNumber !== oldBarNumber) {
|
||||
@@ -205,7 +206,9 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
||||
// Dragged past audio start — snap to earliest allowed position
|
||||
const maxLeftExtensionBeats = clipOffset / secondsPerBeat;
|
||||
const minStartBeat = oldStartBeat - maxLeftExtensionBeats;
|
||||
clampedBarNumber = Math.ceil(minStartBeat / beatsPerBar) + 1;
|
||||
clampedBarNumber = snap
|
||||
? Math.ceil(minStartBeat / beatsPerBar) + 1
|
||||
: (minStartBeat / beatsPerBar) + 1;
|
||||
const oldEndBarNumber = oldBarNumber + (coreRegion.getLength() / beatsPerBar);
|
||||
clampedLength = oldEndBarNumber - clampedBarNumber;
|
||||
newClipStartOffsetSeconds = 0;
|
||||
@@ -219,7 +222,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
||||
const maxDurationSeconds = audioDuration - effectiveClipOffset;
|
||||
const maxLengthBars = (maxDurationSeconds / secondsPerBeat) / beatsPerBar;
|
||||
if (clampedLength > maxLengthBars) {
|
||||
clampedLength = Math.floor(maxLengthBars);
|
||||
clampedLength = snap ? Math.floor(maxLengthBars) : maxLengthBars;
|
||||
if (clampedLength < REGION_CONSTANTS.MIN_REGION_LENGTH) {
|
||||
clampedLength = REGION_CONSTANTS.MIN_REGION_LENGTH;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user