feat: implemented UI of looping feature
This commit is contained in:
@@ -268,6 +268,11 @@ body {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bar-number-cell.looped {
|
||||||
|
background-color: #e1ae01;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
.main-content-body {
|
.main-content-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
|
|||||||
+119
-65
@@ -8,7 +8,7 @@ import TrackInfoPanel from './track/TrackInfoPanel';
|
|||||||
import TrackGridPanel from './track/TrackGridPanel';
|
import TrackGridPanel from './track/TrackGridPanel';
|
||||||
import PianoRoll from './piano-roll/PianoRoll';
|
import PianoRoll from './piano-roll/PianoRoll';
|
||||||
import type { RegionUI } from './interfaces';
|
import type { RegionUI } from './interfaces';
|
||||||
import { DEBUG_MODE } from '../constants';
|
import { DEBUG_MODE, BAR_NUMBERS_CONSTANTS } from '../constants';
|
||||||
import { useRegionOperations } from '../hooks/useRegionOperations';
|
import { useRegionOperations } from '../hooks/useRegionOperations';
|
||||||
import { regionDeleteManager } from '../util/regionDeleteUtil';
|
import { regionDeleteManager } from '../util/regionDeleteUtil';
|
||||||
|
|
||||||
@@ -73,9 +73,11 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
// Refs to track pending updates for verification
|
// Refs to track pending updates for verification
|
||||||
const pendingUpdates = useRef<Map<string, { trackId: string, regionId: string, startBeat: number, length: number }>>(new Map());
|
const pendingUpdates = useRef<Map<string, { trackId: string, regionId: string, startBeat: number, length: number }>>(new Map());
|
||||||
|
|
||||||
// Refs for bar numbers drag functionality
|
// Refs for bar numbers and loop range drag functionality
|
||||||
const isDraggingRef = useRef(false);
|
|
||||||
const barNumbersRef = useRef<HTMLDivElement | null>(null);
|
const barNumbersRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const isLoopDraggingRef = useRef(false);
|
||||||
|
const loopDragStartBarRef = useRef<number | null>(null);
|
||||||
|
const loopDragStartXRef = useRef<number | null>(null);
|
||||||
|
|
||||||
// Effect to verify track updates
|
// Effect to verify track updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -515,75 +517,115 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
return destinationBeatPosition;
|
return destinationBeatPosition;
|
||||||
}, [timeSignature]);
|
}, [timeSignature]);
|
||||||
|
|
||||||
// Handle mouse down to start dragging
|
// Utility function to calculate bar index from mouse coordinates (for loop range selection)
|
||||||
|
const calculateBarIndexFromMouse = useCallback((clientX: number): number | null => {
|
||||||
|
if (!barNumbersRef.current) return null;
|
||||||
|
|
||||||
|
const rect = barNumbersRef.current.getBoundingClientRect();
|
||||||
|
const relativeX = clientX - rect.left;
|
||||||
|
|
||||||
|
// Calculate the width of each bar
|
||||||
|
const barWidth = parseInt(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--track-grid-bar-width')
|
||||||
|
) || 40;
|
||||||
|
|
||||||
|
// Calculate bar index (using Math.floor for exact bar boundaries)
|
||||||
|
const barIndex = Math.floor(relativeX / barWidth);
|
||||||
|
|
||||||
|
// Clamp to valid range [0, maxBars - 1]
|
||||||
|
return Math.max(0, Math.min(barIndex, maxBars - 1));
|
||||||
|
}, [maxBars]);
|
||||||
|
|
||||||
|
// Handle mouse down to start dragging (for loop range selection)
|
||||||
const handleBarNumbersMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
const handleBarNumbersMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
// Only handle left mouse button
|
// Only handle left mouse button
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
isDraggingRef.current = true;
|
// Calculate starting bar index
|
||||||
|
const startBarIndex = calculateBarIndexFromMouse(e.clientX);
|
||||||
// Calculate and set initial playhead position
|
if (startBarIndex === null) return;
|
||||||
const newPosition = calculatePlayheadFromMouse(e.clientX);
|
|
||||||
if (newPosition !== null) {
|
// Always start loop drag tracking
|
||||||
setPlayheadPosition(newPosition);
|
isLoopDraggingRef.current = true;
|
||||||
|
loopDragStartBarRef.current = startBarIndex;
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
loopDragStartXRef.current = e.clientX;
|
||||||
console.log(`Bar numbers drag started - Initial position: ${newPosition} (bar ${Math.floor(newPosition / timeSignature.numerator) + 1})`);
|
|
||||||
}
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
|
console.log(`Bar numbers mouse down - Start bar: ${startBarIndex} (displayed as bar ${startBarIndex + 1})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent text selection during drag
|
// Prevent text selection during drag
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle click on bar numbers to move playhead (when not dragging)
|
// Global mouse move and mouse up handlers for loop range drag functionality
|
||||||
const handleBarNumbersClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
// If we were dragging, don't process as a click
|
|
||||||
if (isDraggingRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPosition = calculatePlayheadFromMouse(e.clientX);
|
|
||||||
if (newPosition !== null) {
|
|
||||||
const core = KGCore.instance();
|
|
||||||
const currentPlayheadPosition = core.getPlayheadPosition();
|
|
||||||
const beatsPerBar = timeSignature.numerator;
|
|
||||||
const currentBarNumber = Math.floor(currentPlayheadPosition / beatsPerBar) + 1; // 1-indexed
|
|
||||||
const destinationBarNumber = Math.floor(newPosition / beatsPerBar) + 1; // 1-indexed
|
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
|
||||||
console.log(`Bar numbers click - Position: ${newPosition}`);
|
|
||||||
console.log(`Current bar: ${currentBarNumber} (beat ${currentPlayheadPosition})`);
|
|
||||||
console.log(`Destination bar: ${destinationBarNumber} (beat ${newPosition})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlayheadPosition(newPosition);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Global mouse move and mouse up handlers for bar numbers drag functionality
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
if (!isDraggingRef.current) return;
|
if (!isLoopDraggingRef.current) return;
|
||||||
|
if (loopDragStartBarRef.current === null || loopDragStartXRef.current === null) return;
|
||||||
const newPosition = calculatePlayheadFromMouse(e.clientX);
|
|
||||||
if (newPosition !== null) {
|
// Calculate distance moved
|
||||||
setPlayheadPosition(newPosition);
|
const distanceMoved = Math.abs(e.clientX - loopDragStartXRef.current);
|
||||||
|
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
// Only update if moved beyond threshold
|
||||||
console.log(`Bar numbers drag - Position: ${newPosition} (bar ${Math.floor(newPosition / timeSignature.numerator) + 1})`);
|
if (distanceMoved < BAR_NUMBERS_CONSTANTS.DRAG_THRESHOLD) return;
|
||||||
}
|
|
||||||
|
// Calculate current bar index
|
||||||
|
const currentBarIndex = calculateBarIndexFromMouse(e.clientX);
|
||||||
|
if (currentBarIndex === null) return;
|
||||||
|
|
||||||
|
// Create loop range [min, max] regardless of drag direction
|
||||||
|
const startBar = loopDragStartBarRef.current;
|
||||||
|
const loopStart = Math.min(startBar, currentBarIndex);
|
||||||
|
const loopEnd = Math.max(startBar, currentBarIndex);
|
||||||
|
const newLoopRange: [number, number] = [loopStart, loopEnd];
|
||||||
|
|
||||||
|
// Update project model
|
||||||
|
const core = KGCore.instance();
|
||||||
|
const project = core.getCurrentProject();
|
||||||
|
project.setLoopingRange(newLoopRange);
|
||||||
|
project.setIsLooping(true); // Enable looping immediately during drag for real-time visual feedback
|
||||||
|
|
||||||
|
// Update store to trigger UI re-render
|
||||||
|
useProjectStore.setState({ loopingRange: newLoopRange, isLooping: true });
|
||||||
|
|
||||||
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
|
console.log(`Loop range drag - Range: [${loopStart}, ${loopEnd}] (bars ${loopStart + 1}-${loopEnd + 1})`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = (e: MouseEvent) => {
|
||||||
if (isDraggingRef.current) {
|
if (isLoopDraggingRef.current) {
|
||||||
isDraggingRef.current = false;
|
if (loopDragStartXRef.current !== null) {
|
||||||
|
const distanceMoved = Math.abs(e.clientX - loopDragStartXRef.current);
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
|
||||||
console.log('Bar numbers drag ended');
|
// If dragged beyond threshold, enable looping
|
||||||
|
if (distanceMoved >= BAR_NUMBERS_CONSTANTS.DRAG_THRESHOLD) {
|
||||||
|
const core = KGCore.instance();
|
||||||
|
const project = core.getCurrentProject();
|
||||||
|
project.setIsLooping(true);
|
||||||
|
useProjectStore.setState({ isLooping: true });
|
||||||
|
|
||||||
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
|
console.log('Loop range drag ended - Looping auto-enabled');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single click (moved < threshold) - set playhead position
|
||||||
|
const clickPosition = calculatePlayheadFromMouse(e.clientX);
|
||||||
|
if (clickPosition !== null) {
|
||||||
|
setPlayheadPosition(clickPosition);
|
||||||
|
|
||||||
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
|
console.log(`Single click on bar numbers - Set playhead to: ${clickPosition}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset drag state
|
||||||
|
isLoopDraggingRef.current = false;
|
||||||
|
loopDragStartBarRef.current = null;
|
||||||
|
loopDragStartXRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -596,25 +638,37 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
};
|
};
|
||||||
}, [calculatePlayheadFromMouse, setPlayheadPosition, timeSignature]);
|
}, [calculateBarIndexFromMouse, calculatePlayheadFromMouse, setPlayheadPosition]);
|
||||||
|
|
||||||
const { showInstrumentSelection } = useProjectStore();
|
const { showInstrumentSelection, isLooping, loopingRange } = useProjectStore();
|
||||||
|
|
||||||
|
// Helper function to check if a bar (0-indexed) is in the loop range
|
||||||
|
const isBarInLoopRange = (barIndex: number): boolean => {
|
||||||
|
if (!isLooping) return false;
|
||||||
|
// Loop range is [startBar, endBar] (0-indexed)
|
||||||
|
// We want to highlight bars from startBar to endBar inclusive
|
||||||
|
return barIndex >= loopingRange[0] && barIndex <= loopingRange[1];
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`main-content${showInstrumentSelection ? ' has-left-instrument' : ''}`}>
|
<div className={`main-content${showInstrumentSelection ? ' has-left-instrument' : ''}`}>
|
||||||
<div className="main-content-wrapper">
|
<div className="main-content-wrapper">
|
||||||
{/* Top-left spacer */}
|
{/* Top-left spacer */}
|
||||||
<div className="top-left-spacer"></div>
|
<div className="top-left-spacer"></div>
|
||||||
|
|
||||||
{/* Bar numbers at the top */}
|
{/* Bar numbers at the top */}
|
||||||
<div
|
<div
|
||||||
className="bar-numbers"
|
className="bar-numbers"
|
||||||
ref={barNumbersRef}
|
ref={barNumbersRef}
|
||||||
onMouseDown={handleBarNumbersMouseDown}
|
onMouseDown={handleBarNumbersMouseDown}
|
||||||
onClick={handleBarNumbersClick}
|
|
||||||
>
|
>
|
||||||
{Array.from({ length: maxBars }, (_, i) => (
|
{Array.from({ length: maxBars }, (_, i) => (
|
||||||
<div key={i} className="bar-number-cell">{i + 1}</div>
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`bar-number-cell${isBarInLoopRange(i) ? ' looped' : ''}`}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { useProjectStore } from '../stores/projectStore';
|
|||||||
import { DEBUG_MODE } from '../constants/uiConstants';
|
import { DEBUG_MODE } from '../constants/uiConstants';
|
||||||
import { TIME_CONSTANTS } from '../constants/coreConstants';
|
import { TIME_CONSTANTS } from '../constants/coreConstants';
|
||||||
import { parseTimeSignature, getTimeSignatureErrorMessage } from '../util/timeUtil';
|
import { parseTimeSignature, getTimeSignatureErrorMessage } from '../util/timeUtil';
|
||||||
import {
|
import {
|
||||||
FaUndo, FaRedo, FaMousePointer, FaStepBackward,
|
FaUndo, FaRedo, FaMousePointer, FaStepBackward,
|
||||||
FaPlay, FaPause, FaComments,
|
FaPlay, FaPause, FaComments, FaSync,
|
||||||
FaFolderOpen, FaSave, FaDownload, FaUpload, FaPlus,
|
FaFolderOpen, FaSave, FaDownload, FaUpload, FaPlus,
|
||||||
FaCog
|
FaCog
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
@@ -27,12 +27,13 @@ import { clearChatHistoryAndUI } from '../util/chatUtil';
|
|||||||
import PianoIcon from './common/icons/PianoIcon';
|
import PianoIcon from './common/icons/PianoIcon';
|
||||||
|
|
||||||
const Toolbar: React.FC = () => {
|
const Toolbar: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
projectName, setProjectName,
|
projectName, setProjectName,
|
||||||
bpm, timeSignature, keySignature, setStatus,
|
bpm, timeSignature, keySignature, setStatus,
|
||||||
isPlaying, startPlaying, stopPlaying, setPlayheadPosition,
|
isPlaying, startPlaying, stopPlaying, setPlayheadPosition,
|
||||||
currentTime, setBpm, setTimeSignature, setKeySignature,
|
currentTime, setBpm, setTimeSignature, setKeySignature,
|
||||||
maxBars, setMaxBars,
|
maxBars, setMaxBars,
|
||||||
|
isLooping, loopingRange,
|
||||||
canUndo, canRedo, undoDescription, redoDescription, undo, redo,
|
canUndo, canRedo, undoDescription, redoDescription, undo, redo,
|
||||||
toggleChatBox, toggleSettings, cleanupProjectState,
|
toggleChatBox, toggleSettings, cleanupProjectState,
|
||||||
// Piano roll state/actions
|
// Piano roll state/actions
|
||||||
@@ -43,7 +44,7 @@ const Toolbar: React.FC = () => {
|
|||||||
|
|
||||||
// State for main content tools
|
// State for main content tools
|
||||||
const [activeMainTool, setActiveMainTool] = React.useState<'pointer' | 'pencil'>('pointer');
|
const [activeMainTool, setActiveMainTool] = React.useState<'pointer' | 'pencil'>('pointer');
|
||||||
|
|
||||||
// State for key signature dropdown
|
// State for key signature dropdown
|
||||||
const [showKeySignatureDropdown, setShowKeySignatureDropdown] = React.useState(false);
|
const [showKeySignatureDropdown, setShowKeySignatureDropdown] = React.useState(false);
|
||||||
|
|
||||||
@@ -412,6 +413,50 @@ const Toolbar: React.FC = () => {
|
|||||||
setPlayheadPosition(0);
|
setPlayheadPosition(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLoopToggle = () => {
|
||||||
|
const core = KGCore.instance();
|
||||||
|
const project = core.getCurrentProject();
|
||||||
|
const newLoopingState = !isLooping;
|
||||||
|
let newLoopingRange = loopingRange;
|
||||||
|
|
||||||
|
// When enabling loop, validate and set the loop range
|
||||||
|
if (newLoopingState) {
|
||||||
|
const currentRange = loopingRange;
|
||||||
|
const projectMaxBars = maxBars;
|
||||||
|
|
||||||
|
// If range is [0, 0], set it to the entire song
|
||||||
|
if (currentRange[0] === 0 && currentRange[1] === 0) {
|
||||||
|
newLoopingRange = [0, projectMaxBars] as [number, number];
|
||||||
|
if (DEBUG_MODE.TOOLBAR) {
|
||||||
|
console.log("Loop range auto-set to entire song:", newLoopingRange);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Validate range is within [0, maxBars]
|
||||||
|
const validatedStart = Math.max(0, Math.min(currentRange[0], projectMaxBars));
|
||||||
|
const validatedEnd = Math.max(0, Math.min(currentRange[1], projectMaxBars));
|
||||||
|
|
||||||
|
// If range changed, update it
|
||||||
|
if (validatedStart !== currentRange[0] || validatedEnd !== currentRange[1]) {
|
||||||
|
newLoopingRange = [validatedStart, validatedEnd] as [number, number];
|
||||||
|
if (DEBUG_MODE.TOOLBAR) {
|
||||||
|
console.log("Loop range clamped to valid range:", newLoopingRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update project model
|
||||||
|
project.setIsLooping(newLoopingState);
|
||||||
|
project.setLoopingRange(newLoopingRange);
|
||||||
|
|
||||||
|
// Update store to trigger UI re-render
|
||||||
|
useProjectStore.setState({ isLooping: newLoopingState, loopingRange: newLoopingRange });
|
||||||
|
|
||||||
|
if (DEBUG_MODE.TOOLBAR) {
|
||||||
|
console.log("Loop toggle clicked, isLooping:", newLoopingState, "range:", newLoopingRange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Prompt to change max bars when clicking on current-time display
|
// Prompt to change max bars when clicking on current-time display
|
||||||
const handleCurrentTimeClick = () => {
|
const handleCurrentTimeClick = () => {
|
||||||
const MIN_BARS = 16;
|
const MIN_BARS = 16;
|
||||||
@@ -740,6 +785,13 @@ const Toolbar: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<button title="Pause" className="button-pause" onClick={handlePauseClick}><FaPause /></button>
|
<button title="Pause" className="button-pause" onClick={handlePauseClick}><FaPause /></button>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
title="Loop"
|
||||||
|
className={`tool-button ${isLooping ? 'active' : ''}`}
|
||||||
|
onClick={handleLoopToggle}
|
||||||
|
>
|
||||||
|
<FaSync />
|
||||||
|
</button>
|
||||||
<div className="toolbar-separator"></div>
|
<div className="toolbar-separator"></div>
|
||||||
<button title="Piano" onClick={handlePianoButtonClick}><PianoIcon /></button>
|
<button title="Piano" onClick={handlePianoButtonClick}><PianoIcon /></button>
|
||||||
{/* <button title="Record"><FaCircle className="record-btn" /></button>
|
{/* <button title="Record"><FaCircle className="record-btn" /></button>
|
||||||
|
|||||||
@@ -51,3 +51,9 @@ export const PLAYING_CONSTANTS = {
|
|||||||
// Update rate for playback (10 FPS for performance evaluation)
|
// Update rate for playback (10 FPS for performance evaluation)
|
||||||
UPDATE_INTERVAL_MS: 100, // 1000ms / 10fps = 100ms
|
UPDATE_INTERVAL_MS: 100, // 1000ms / 10fps = 100ms
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bar numbers related constants
|
||||||
|
export const BAR_NUMBERS_CONSTANTS = {
|
||||||
|
// Minimum drag distance (in pixels) to consider as drag vs click
|
||||||
|
DRAG_THRESHOLD: 3,
|
||||||
|
};
|
||||||
|
|||||||
+27
-1
@@ -35,6 +35,14 @@ export class KGProject {
|
|||||||
@WithDefault("ionian")
|
@WithDefault("ionian")
|
||||||
private selectedMode: string = "ionian";
|
private selectedMode: string = "ionian";
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@WithDefault(false)
|
||||||
|
private isLooping: boolean = false;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@WithDefault([0, 0])
|
||||||
|
private loopingRange: [number, number] = [0, 0]; // [startBar, endBar] - bar indices (0-based)
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
@WithDefault(0)
|
@WithDefault(0)
|
||||||
private projectStructureVersion: number = 0;
|
private projectStructureVersion: number = 0;
|
||||||
@@ -54,7 +62,7 @@ export class KGProject {
|
|||||||
private tracks: KGTrack[] = [];
|
private tracks: KGTrack[] = [];
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(name: string = "Untitled Project", maxBars: number = 32, currentBars: number = 0, bpm: number = 125, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION) {
|
constructor(name: string = "Untitled Project", maxBars: number = 32, currentBars: number = 0, bpm: number = 125, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.maxBars = maxBars;
|
this.maxBars = maxBars;
|
||||||
this.currentBars = currentBars;
|
this.currentBars = currentBars;
|
||||||
@@ -62,6 +70,8 @@ export class KGProject {
|
|||||||
this.timeSignature = timeSignature;
|
this.timeSignature = timeSignature;
|
||||||
this.keySignature = keySignature;
|
this.keySignature = keySignature;
|
||||||
this.selectedMode = selectedMode;
|
this.selectedMode = selectedMode;
|
||||||
|
this.isLooping = isLooping;
|
||||||
|
this.loopingRange = loopingRange;
|
||||||
this.tracks = tracks;
|
this.tracks = tracks;
|
||||||
this.projectStructureVersion = projectStructureVersion;
|
this.projectStructureVersion = projectStructureVersion;
|
||||||
}
|
}
|
||||||
@@ -139,5 +149,21 @@ export class KGProject {
|
|||||||
public getProjectStructureVersion(): number {
|
public getProjectStructureVersion(): number {
|
||||||
return this.projectStructureVersion;
|
return this.projectStructureVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getIsLooping(): boolean {
|
||||||
|
return this.isLooping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setIsLooping(isLooping: boolean): void {
|
||||||
|
this.isLooping = isLooping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLoopingRange(): [number, number] {
|
||||||
|
return this.loopingRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLoopingRange(loopingRange: [number, number]): void {
|
||||||
|
this.loopingRange = loopingRange;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ interface ProjectState {
|
|||||||
bpm: number;
|
bpm: number;
|
||||||
keySignature: KeySignature;
|
keySignature: KeySignature;
|
||||||
selectedMode: string;
|
selectedMode: string;
|
||||||
|
isLooping: boolean;
|
||||||
|
loopingRange: [number, number]; // [startBar, endBar] - bar indices (0-based)
|
||||||
playheadPosition: number; // in beats
|
playheadPosition: number; // in beats
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
currentTime: string; // formatted time string
|
currentTime: string; // formatted time string
|
||||||
@@ -225,6 +227,8 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
bpm: currentProject.getBpm(),
|
bpm: currentProject.getBpm(),
|
||||||
keySignature: currentProject.getKeySignature(),
|
keySignature: currentProject.getKeySignature(),
|
||||||
selectedMode: currentProject.getSelectedMode(),
|
selectedMode: currentProject.getSelectedMode(),
|
||||||
|
isLooping: currentProject.getIsLooping(),
|
||||||
|
loopingRange: currentProject.getLoopingRange(),
|
||||||
playheadPosition: KGCore.instance().getPlayheadPosition(),
|
playheadPosition: KGCore.instance().getPlayheadPosition(),
|
||||||
isPlaying: KGCore.instance().getIsPlaying(),
|
isPlaying: KGCore.instance().getIsPlaying(),
|
||||||
currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()),
|
currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()),
|
||||||
@@ -519,6 +523,8 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
bpm,
|
bpm,
|
||||||
keySignature,
|
keySignature,
|
||||||
selectedMode: projectToLoad.getSelectedMode(),
|
selectedMode: projectToLoad.getSelectedMode(),
|
||||||
|
isLooping: projectToLoad.getIsLooping(),
|
||||||
|
loopingRange: projectToLoad.getLoopingRange(),
|
||||||
playheadPosition: 0, // Ensure store state is also updated
|
playheadPosition: 0, // Ensure store state is also updated
|
||||||
currentTime: beatsToTimeString(0, bpm, timeSignature) // Reset time display
|
currentTime: beatsToTimeString(0, bpm, timeSignature) // Reset time display
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ export const createMockProject = (overrides: Partial<{
|
|||||||
defaults.timeSignature,
|
defaults.timeSignature,
|
||||||
'C major', // keySignature
|
'C major', // keySignature
|
||||||
'ionian', // selectedMode
|
'ionian', // selectedMode
|
||||||
|
false, // isLooping
|
||||||
|
[0, 0], // loopingRange
|
||||||
defaults.tracks, // tracks
|
defaults.tracks, // tracks
|
||||||
1 // projectStructureVersion
|
1 // projectStructureVersion
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user