Files
SonicForgeStudio/wiki.md
T

22 KiB

[2026-07-25 06:57] Task: Fix Piano Roll runtime errors (rollBeats TDZ + handleCCMouseMove)

  • Tóm tắt thay đổi: Sửa lỗi Cannot access 'rollBeats' before initialization bằng cách di chuyển khai báo rollBeats/rollBeatsRef lên trước totalBeats. Thêm hàm handleCCMouseMove bị thiếu và cập nhật handleCCMouseDown hỗ trợ Ctrl+Click paint velocity.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): cd /home/locpham/SonicForgeStudio && npm run build — build passes.

[2026-07-25 07:25] Task: 4 tính năng mới (Piano Roll zoom fill + Shift+scroll playhead + auto-scroll drag + SoundFont cache)

  • Tóm tắt thay đổi: (1) Piano Roll zoom-out không còn màn hình đen — bars fill toàn bộ viewport. (2) Shift+scroll trong Piano Roll di chuyển playhead và play notes MIDI như fast-forward. (3) Khi drag section/MIDI/clip đến cạnh phải timeline, auto-scroll container. (4) Server-side cache FluidSynth instances + PluginManager singleton + list_soundfont_instruments cache.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/core/vst_engine.py
  • Ghi chú/Test (nếu có): npm run build pass. vst_engine.py thêm load_soundfont_cached, release_soundfont, get_plugin_manager, _SF_INSTRUMENTS_CACHE — refcount-based cache.

[2026-07-26 09:36] Task: Piano Roll 4 features (SNAP, velocity selected, auto-scroll, Synth button)

  • Tóm tắt thay đổi: (1) SNAP trong MIDI tab — grid snap cho note drawing, selection marquee, loop range. (2) Ctrl+drag velocity — selected notes màu xanh dương, affect only selected; no selection = paint all. (3) Auto-scroll Piano Roll khi brush-drag gần cạnh top/bottom. (4) Synth button trong MIDI tab toolbar — mở instrument selector khi nhấn. Layout fix: thêm h-full cho outer div.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass. Fix thiếu 1 ) tại line 5959 return statement (gây build error pre-existing).

[2026-07-26 10:08] Task: Fix auto-scroll continuous + CC velocity batch + flicker

  • Tóm tắt thay đổi: (1) Brush-draw gần cạnh top/bottom giờ dùng setInterval 30ms để cuộn liên tục (trước đây cuộn 1 lần per mousemove). Stop interval khi mouseup hoặc chuột rời khỏi threshold. (2) Ctrl+drag velocity cho selected notes — gộp tất cả setNotes vào 1 batch call (thay vì 1 call per note), tránh flicker do multi re-render. (3) handleCCMouseDown selectedMode cũng batch setNotes.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass. Thêm ref brushAutoScrollRef cho setInterval auto-scroll. Batch setNotes trong cả handleCCMouseDownhandleCCMouseMove để tránh flicker.

[2026-07-26 10:12] Task: Fix brush pitch-based scroll + CC velocity per-note paint

  • Tóm tắt thay đổi: (1) Brush auto-scroll: chuyển từ scroll theo viewport edge (clientY) sang pitch-based — scroll khi note mới được vẽ ở pitch gần rìa visible area (6 note threshold). Dùng NoteHeight * 2 scroll step, setInterval 30ms. (2) CC velocity selected mode: thay vì paint all selected notes cùng lúc, paint từng note một theo vị trí chuột — tìm note gần cursor beat nhất, chỉ paint note đó nếu đang được selected.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass. Thay lastPainted trong selectedMode từ array all indices thành array single-node index.

[2026-07-26 10:20] Task: Fix brush scroll speed + CC snap + non-selected flicker

  • Tóm tắt thay đổi: (1) Giảm auto-scroll step từ NoteHeight * 2 xuống max(1, floor(NoteHeight * 0.5)) — cuộn chậm hơn để draw note đúng pitch. (2) CC lane handleCCMouseDownhandleCCMouseMove dùng getSnapBeat(beat, snapVal) khi tìm note index — snap đúng grid. (3) Non-selected CC flicker: xóa paintNote helper, inline setNotes trực tiếp trong mousedown, không gọi setNotes riêng lẻ gây re-render dư thừa.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass.

[2026-07-25 07:25] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin

  • Tóm tắt thay đổi: (1) maxDuration dùng activeTracks + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng updateActiveTracks. (5) Stretched clip handler thêm auto-scroll.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 07:31] Task: Fix zoom cursor + right edge black + AI Copilot for Piano Roll

  • Tóm tắt thay đổi: (1) Tách drawWidth (content) và cssWidth (CSS) — canvas coordinate system không còn bị sai khi zoom out. (2) Thêm min-width fill viewport, không còn mảng đen phải. (3) Thêm AI Copilot input + handler trong Piano Roll — gõ prompt tạo MIDI notes trực tiếp.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass.

[2026-07-25 08:08] Task: Fix velocity draw position, shift+scroll play, canvas redraw deps

  • Tóm tắt thay đổi: (1) CC lane velocity stem vẽ tại start note thay vì center. (2) Shift+scroll chỉ play note khi playhead vượt qua start_beat. (3) Thêm viewWidth/viewBeats vào dependency array grid canvas + CC canvas để auto-redraw khi resize/zoom.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build pass.

[2026-07-25 08:31] Task: Fix AI Copilot tab scope + hide bottom panels + context menu Insert + autoscroll

  • Tóm tắt thay đổi: (1) AI Copilot context dùng activeTracks thay tracks — chỉ tác động tab đang active. (2) Bottom panels mặc định ẩn. (3) Right-click track timeline có 4 mục Insert. (4) Thêm auto-scroll vào stretched clip resize. Sửa insertSectionAtPlayhead dùng updateActiveTracks.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 08:36] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin

  • Tóm tắt thay đổi: (1) maxDuration dùng activeTracks + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng updateActiveTracks. (5) Stretched clip handler thêm auto-scroll.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 08:53] Task: Add timeline left/right margin + dynamic buffer expansion

  • Tóm tắt thay đổi: (1) PADDING_LEFT thay đổi từ 2s thành 2-bar margin ở cả WaveformLane và TempoTrackLane. (2) Thêm state scrollBufferExtra + auto-expand 4 bars khi auto-scroll gần đến rìa timeline. (3) maxDuration thêm scrollBufferExtra + 6 bars static buffer. (4) Clip drag/stretch dùng activeTracks + updateActiveTracks thay tracks/setTracks.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 09:04] Task: Fix left margin 1-beat, items always visible, bar/timeline sync

  • Tóm tắt thay đổi: (1) PADDING_LEFT từ 2-bar xuống 1-beat ở cả WaveformLane và TempoTrackLane. (2) Drag clamp left ≥ 1 beat, right ≤ maxDuration - 1 bar. (3) Bar numbers + grid vẽ lại đồng bộ với scroll+zoom.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 09:08] Task: Fix Piano Roll CC lane always visible + resizable height

  • Tóm tắt thay đổi: (1) showCC default true — CC lane luôn hiển thị. (2) Thêm ccHeight state + resize handle (drag top edge) — cho phép thay đổi độ cao CC lane. (3) Canvas dùng ccHeight thay hardcoded 80px.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 09:18] Task: Fix brush velocity isolation from CC lane

  • Tóm tắt thay đổi: Thêm brushVelocityRef riêng biệt (0.8) — brush không còn bị ảnh hưởng bởi velocity paint từ CC lane.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 09:20] Task: Auto-follow dragged item in viewport

  • Tóm tắt thay đổi: (1) Clip drag: sau mỗi update, nếu item ra khỏi vùng 120px viewport edge, tự scroll để item luôn thấy. (2) Section/MIDI drag: tương tự. (3) Section/MIDI resize: theo dõi edge đang kéo.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 09:23] Task: Sync bar ruler redraw during drag

  • Tóm tắt thay đổi: (1) TempoTrackLane nhận canvasRedrawCount prop + deps — force redraw bar numbers khi drag. (2) Cả 3 drag handler increment setCanvasRedrawCount sau auto-follow scroll. (3) Sub-tab TempoTrackLane thêm viewportWidth + scrollLeft + canvasRedrawCount — đồng bộ ruler với timeline.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 09:30] Task: Fix bar numbers disappear past bar 12 during drag

  • Tóm tắt thay đổi: TempoTrackLane đọc scrollLeft từ DOM (walk parent chain tìm scroll container) thay vì React prop — canvas luôn dùng scroll position thực tế, không lag do async state.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có):

[2026-07-25 09:58] Task: Fix drag speed + bar numbers visibility + snap section items

  • Tóm tắt thay đổi: (1) Xoá auto-follow phải (gây jump), chỉ giữ auto-follow trái 60px — scroll phải nhờ autoScrollTimeline mượt. (2) Thêm PADDING_RIGHT vào tEnd ở TempoTrackLane và WaveformLane — bar numbers + grid hiển thị đến sát rìa phải. (3) if (localX < 0 || localX > drawWidth) continue; — skip phần tử ngoài canvas. (4) Buffer 12 bars cho maxDuration. (5) Thêm snapTime vào section/MIDI item drag. (6) Thêm snap option '4' (1 bar). (7) Đồng bộ PADDING_LEFT = barDuration ở cả 2 lane.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npx babel app/static/js/app.jsx — compile pass.

[2026-07-25 10:08] Task: Merge tempo bar + timebar, beat-based ruler, snap grid

  • Tóm tắt thay đổi: Thay thế seconds-based ruler (0.00s, 1.00s...) + TempoTrackLane bằng TimelineRuler (48px canvas) hiển thị bar:beat grid lines + snap sub-ticks + BPM label. Grid WaveformLane khớp ruler, snapTime() đã dùng grid spacing beat — snap items đúng vị trí ruler.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 10:35] Task: Restore time duration labels on TimelineRuler + TempoTrackLane

  • Tóm tắt thay đổi: Thêm time duration labels (formatTimeSimple: "0.00s", "1.00s",...) vào TimelineRuler và TempoTrackLane. Hiển thị dạng màu cam (#ffb464) ở chân ruler, động điều chỉnh interval (1s/2s/5s/10s...) theo zoom level. Time labels đã bị mất từ commit ff44fd4 khi merge time bar + tempo bar thành TimelineRuler.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 10:39] Task: Sync TempoTrackLane leadIn + time labels on ruler top

  • Tóm tắt thay đổi: Thêm leadInMargin prop vào TempoTrackLane để đồng bộ column-0 (time=0) với vị trí leadIn của TimelineRuler. Thêm CLIP_BUFFER vào TempoTrackLane. Di chuyển time duration labels trong TimelineRuler từ dưới đáy (y=40) lên phía trên (y=11).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 11:09] Task: Fix TempoTrackLane scroll sync during drag auto-scroll

  • Tóm tắt thay đổi: TempoTrackLane/WaveformLane/TimelineRuler hiện đọc scrollLeft trực tiếp từ DOM wrapper (canvases tìm parent scrollable) thay vì React prop. Tránh lệch vị trí khi auto-scroll nhanh trong clip drag do React state batching.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 11:04] Task: Fix PADDING_LEFT alignment + right-side bar rendering

  • Tóm tắt thay đổi: Set PADDING_LEFT=0 trong TimelineRuler/WaveformLane/TempoTrackLane để grid column 0 khớp với clip position 0 (trước đây lệch 8px). TempoTrackLane dùng CLIP_BUFFER/zoom cho tEnd thay vì PADDING_LEFT để bar/beat vẽ đủ xa về phía phải khi scroll.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 10:58] Task: Fix clip/section snap to column 0

  • Tóm tắt thay đổi: Xóa Math.max(beatSec, ...) trong clip drag (10325), section drag (10495), section resize (10576) — thay bằng Math.max(0, ...) để item có thể snap vào column 0.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 10:54] Task: Fix TempoTrackLane height + clamp selection at column 0

  • Tóm tắt thay đổi: Fix TempoTrackLane canvas height = 40px cố định (thay vì parentElement.clientHeight gây tràn). Wrapper h-10 shrink-0 overflow-hidden để không đẩy track lanes lệch TCP. Clamp selLeft/selRight/ruler handlers >= 0 để selection không vào vùng âm.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 10:44] Task: Zero lead-in, time ruler + tempo track split layout

  • Tóm tắt thay đổi: Xóa leadInMargin (về 0) — time 0 tại mép trái, chỉ còn 8px PADDING_LEFT. TimelineRuler đơn giản thành time-only ruler (40px, chỉ hiển thị time labels). Thêm TempoTrackLane ngay dưới (40px, bars/beats/BPM). Selection overlay top từ 48→80px. Đồng bộ WaveformLane PADDING_LEFT=8/zoom.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 10:18] Task: Lead-in margin, bar 0-indexed, grid clip fix

  • Tóm tắt thay đổi: Thêm leadInMargin (1 bar) đẩy timeline content sang phải — bar 0, time 0s tại mép margin. Playhead bắt đầu từ đó. Bar numbering 0-indexed (bar 0 thay vì 1). Sửa CLIP_BUFFER từ 200px → max(400, barWidth+200) để bar number không bị che. Đồng bộ leadInMargin vào tStart/tEnd canvas + mouse handlers (ruler, clip drag, section resize).
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 11:32] Task: Redraw canvas on section item duration resize

  • Tóm tắt thay đổi: Thêm setCanvasRedrawCount(n => n + 1) trong resize mousemove handler để WaveformLane redraw khi section/MIDI item duration thay đổi.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 17:22] Task: Piano roll toolbar - transport, ARM/INPUT, AI bar range

  • Tóm tắt thay đổi: Thêm transport (play/stop/back/next/record), ARM+INPUT select MIDI, AI bar range (bar x-y) vào piano roll toolbar. handleAISend phát hiện piano roll active → MIDI prompt. Thêm selectedMidiInputId + handleMidiInputSelect.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-25 22:05] Task: Fix piano roll selection, playback, marquee drag/resize/move

  • Tóm tắt thay đổi: (1) Fix playback duration — min 16 beats when no notes exist. (2) Selection marquee: green transparent (#22c55e), full-height from top to CC lane. (3) Document-level mousemove/mouseup for marquee drag (không dừng khi ra khỏi grid). (4) Edge resize handles (left/right) cho marquee. (5) Body drag (move) cho marquee. Hiển thị marquee trên cả grid canvas và CC canvas.
  • Các file ảnh hưởng: app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): node --check app/static/js/app.precompiled.js — syntax OK.

[2026-07-25 22:18] Task: Fix marquee mouseleave, loop reschedule, tempo drag

  • Tóm tắt thay đổi: (1) Separate onMouseLeave from onMouseUp for grid canvas — preserve marquee when mouse leaves canvas. (2) Add MIDI note rescheduling on piano roll loop restart — 2nd+ loop iteration now plays sound. (3) Tempo bar drag already uses document-level listeners — works across columns.
  • Các file ảnh hưởng: app/static/js/app.precompiled.js

[2026-07-26 10:24] Task: Fix scroll speed match mouse + CC flicker useLayoutEffect

  • Tóm tắt thay đổi: (1) Brush auto-scroll: thêm direct scroll target — mỗi mousemove tính pitchPixel, nếu gần rìa visible area (2 note margin) thì scroll trực tiếp tới target để tốc độ cuộn = tốc độ chuột. Giữ interval 16ms fallback cho trường hợp chuột ở rìa viewport. (2) CC flicker: đổi React.useEffectReact.useLayoutEffect cho CC canvas rendering effect — canvas paint trước browser paint, loại bỏ flash khi state thay đổi.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass. Direct scroll target: container.scrollTop = target trên mỗi mousemove khi pitch ở gần rìa.

[2026-07-26 10:28] Task: Fix velocity flicker - grid canvas useLayoutEffect

  • Tóm tắt thay đổi: Đổi grid canvas effect từ React.useEffectReact.useLayoutEffect. Nguyên nhân flicker: khi velocity thay đổi, grid canvas (useEffect) vẽ async sau paint → hiển thị velocity fill cũ 1 frame, trong khi CC canvas (useLayoutEffect) đã vẽ sync trước paint → hiển thị stem mới. Sự mismatch giữa 2 canvas tạo flicker. Fix: cả grid + CC canvas đều dùng useLayoutEffect, đồng bộ trước paint.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass.

[2026-07-26 10:39] Task: Fix brush shape draw + remove transport + loop repeat sound

  • Tóm tắt thay đổi: (1) Brush drawing: thay vì redistribute notes evenly, append từng note mới tại beat position thực tế (getSnapBeat(beat, snapVal)) — notes theo đúng hình dạng brush stroke. (2) Xóa transport controls (back/play/stop/record/forward) khỏi MIDI tab toolbar — chỉ dùng global transport. (3) Loop repeat: thêm schedulePianoRollMidi helper — reschedule MIDI notes qua SonicSF mỗi khi loop repeat, fix lỗi notes trong vùng chọn không có âm thanh khi play lặp lại.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass.

[2026-07-26 10:44] Task: Fix brush drawing smooth - track pitch + beat delta

  • Tóm tắt thay đổi: Brush drawing: theo dõi cả lastDrawBeatvisitedPitches. Tạo note mới khi pitch thay đổi HOẶC beat di chuyển >= minBeatStep (25% của grid duration). Dùng raw beat thay vì getSnapBeat để note theo sát vị trí chuột. Khởi tạo lastDrawBeat: start trong draggedNote. Fix lỗi draw bị ngắt quãng do chỉ tạo note khi pitch thay đổi.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass.

[2026-07-26 10:47] Task: Fix brush draw only pitch change + CC velocity flicker skip

  • Tóm tắt thay đổi: (1) Brush draw: chỉ tạo note mới khi pitchChanged (pitch chưa visited). Nếu cùng pitch, extend duration_beats của note cuối cùng trong brushIds — không tạo note mới trùng pitch. (2) CC velocity: thêm check Math.abs(currentVal - val) > 0.001 trước mỗi setNotes — skip render khi velocity/pan không thay đổi, loại bỏ flicker cho cả selected + non-selected mode.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass. currentVal tính: velocity = note.velocity || 0.8; pan = (note.pan || 0) / 2 + 0.5.

[2026-07-26 10:49] Task: Fix brush draw per-note duration + noteStartBeats tracking

  • Tóm tắt thay đổi: (1) Thêm noteStartBeats array trong draggedNote — track start beat của từng note. Khi tạo note mới (pitchChanged), fix duration của note trước đó thành beat - prevNoteBeat. Khi extend (cùng pitch), chỉ extend note cuối với beat - lastNoteBeat (không extend toàn bộ stroke). (2) Khởi tạo noteStartBeats: [start] trong setDraggedNote. Fix lỗi note cuối có duration dài bằng toàn bộ brush stroke.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass.

[2026-07-26 10:55] Task: Fix brush draw pitch revisit by comparing lastDrawnPitch

  • Tóm tắt thay đổi: Thay !visited.includes(snappedPitch) bằng snappedPitch !== lastDrawnPitch. Bug cũ: khi vẽ C→D→C, visited.includes(C) = true → không tạo note mới → note D cũ bị extend. Fix: so sánh với pitch của note cuối cùng, cho phép vẽ tiếp khi pitch thay đổi dù đã visit trước đó. Bỏ visitedPitches (không còn dùng).
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build pass.