Skip to content

refactor: decompose PianoRollCanvas.tsx into focused modules#1050

Merged
ChuxiJ merged 1 commit intomainfrom
refactor/issue-1028-pianorollcanvas
Mar 27, 2026
Merged

refactor: decompose PianoRollCanvas.tsx into focused modules#1050
ChuxiJ merged 1 commit intomainfrom
refactor/issue-1028-pianorollcanvas

Conversation

@ChuxiJ
Copy link
Copy Markdown

@ChuxiJ ChuxiJ commented Mar 27, 2026

Summary

  • Extract pure canvas drawing functions into PianoRollRenderer.ts (388 lines) — grid, notes, ghost notes, playhead, box selection, tool badge
  • Extract drag state machine into usePianoRollDrag.ts (565 lines) — mouse down/move/up handlers for all 5 edit modes (select, pencil, paint, erase, velocity paint, slide)
  • Reduce PianoRollCanvas.tsx from 1391 to 784 lines while preserving identical behavior and exported API
  • Update pianoRollBackground.test.tsx to reference the new renderer module for source-level color assertions

Closes #1028

Test plan

  • npx tsc --noEmit — 0 type errors
  • npm test -- --run — all 3008 tests pass (0 failures)
  • npm run build — succeeds

🤖 Generated with Claude Code

Extract pure canvas rendering into PianoRollRenderer.ts (388 lines) and
drag state machine into usePianoRollDrag.ts (565 lines), reducing the
main component from 1391 to 784 lines.

Closes #1028

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 27, 2026 13:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the piano roll implementation by extracting canvas rendering and the drag/mouse interaction state machine out of PianoRollCanvas.tsx into dedicated modules, while keeping behavior and the public API consistent.

Changes:

  • Extracted canvas drawing logic into PianoRollRenderer.ts.
  • Extracted the piano roll drag/state machine into usePianoRollDrag.ts.
  • Updated the background color unit test to assert against the new renderer source.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/components/pianoroll/PianoRollCanvas.tsx Removes inlined rendering + drag logic and delegates to the new renderer/hook modules.
src/components/pianoroll/PianoRollRenderer.ts New pure rendering module responsible for drawing grid, notes, playhead, selection box, tool badge, and velocity lane.
src/components/pianoroll/usePianoRollDrag.ts New hook encapsulating mouse/keyboard drag behavior across tools and edit modes.
tests/unit/pianoRollBackground.test.tsx Updates raw-source import target from canvas component to renderer module for background color assertion.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +270 to +274
// --- Drag state machine (extracted hook) ---

const {
dragRef,
dividerDragRef,
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR closes #1028, but the issue acceptance criteria calls out reducing PianoRollCanvas.tsx to < 400 lines (and potentially extracting additional modules like a velocity lane renderer). After this refactor, the file is still ~785 lines. If the issue should remain open for the remaining extraction work, consider adjusting the “Closes #1028” language or opening a follow-up issue/PR for the remaining scope.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +12
import { useCallback, useEffect, useRef, useState } from 'react';
import type { Clip, MidiNote, PianoRollGrid } from '../../types/project';
import type { PianoRollTool } from './PianoRollConstants';
import {
generateNoteId,
gridSizeToBeats,
MIDI_MAX_NOTE,
PIANO_KEYBOARD_WIDTH,
VELOCITY_LANE_HEIGHT,
} from './PianoRollConstants';
import { useProjectStore } from '../../store/projectStore';
import { useUIStore } from '../../store/uiStore';
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usePianoRollDrag.ts has unused imports/selectors (e.g. useState, generateNoteId, VELOCITY_LANE_HEIGHT, useUIStore, and addMidiNote). This adds dead code and may break lint rules. Please remove the unused imports/selectors, or wire them up if they’re intended to be used.

Suggested change
import { useCallback, useEffect, useRef, useState } from 'react';
import type { Clip, MidiNote, PianoRollGrid } from '../../types/project';
import type { PianoRollTool } from './PianoRollConstants';
import {
generateNoteId,
gridSizeToBeats,
MIDI_MAX_NOTE,
PIANO_KEYBOARD_WIDTH,
VELOCITY_LANE_HEIGHT,
} from './PianoRollConstants';
import { useProjectStore } from '../../store/projectStore';
import { useUIStore } from '../../store/uiStore';
import { useCallback, useEffect, useRef } from 'react';
import type { Clip, MidiNote, PianoRollGrid } from '../../types/project';
import type { PianoRollTool } from './PianoRollConstants';
import {
gridSizeToBeats,
MIDI_MAX_NOTE,
PIANO_KEYBOARD_WIDTH,
} from './PianoRollConstants';
import { useProjectStore } from '../../store/projectStore';

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +37
bpm: number;
clipStartTime: number;
clipDuration: number;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In PianoRollDrawParams, currentBeat is actually treated as a transport time in seconds (it’s multiplied by bpm/60 and offset by clipStartTime). The name is misleading and makes it easy to pass an already-converted beat value by mistake. Consider renaming it to something like currentTime / transportTimeSeconds (and clarifying that clipStartTime/clipDuration are seconds) to prevent future misuse.

Suggested change
bpm: number;
clipStartTime: number;
clipDuration: number;
bpm: number;
// Start time of this clip in transport seconds (not beats).
clipStartTime: number;
// Duration of this clip in transport seconds (not beats).
clipDuration: number;
/**
* Current transport position in seconds.
* NOTE: Despite the name, this is a time value (seconds), not a beat index.
*/

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
import type { MidiNote } from '../../types/project';
import type { GhostNote } from './PianoRollCanvas';
import { drawPianoRollKeyboard } from './PianoRollKeyboard';
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PianoRollRenderer.ts imports the GhostNote type from PianoRollCanvas.tsx. Even though it’s a type-only import, it couples the pure renderer module back to the React component module (and makes the type harder to reuse elsewhere). Consider moving GhostNote into a small shared types file (e.g. pianoRollTypes.ts) that both modules can import from.

Copilot uses AI. Check for mistakes.
@ChuxiJ ChuxiJ merged commit 76c8dba into main Mar 27, 2026
8 checks passed
ChuxiJ pushed a commit that referenced this pull request Mar 27, 2026
…erged

- Marked #1100 and #1101 as already implemented (closed)
- Merged 4 refactoring PRs: #1046, #1049, #1050, #1052
- Updated analysis to reflect current state

https://claude.ai/code/session_01KYEjWp985CYRCcSm9J46ps
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: Decompose PianoRollCanvas.tsx (1391 lines)

2 participants