feat: lead-in margin, 0-indexed bars, fix bar number clipping

- Add 1-bar lead-in margin at timeline start (bar 0, time 0s at margin edge)
- Playhead position includes lead-in offset
- Bar numbers now 0-indexed (0, 1, 2...) in ruler and track grid
- Fix bar number clipping: CLIP_BUFFER increased from 200px to
  max(400px, barWidth+200px) so bars past viewport edge render
- All mouse/time handlers account for lead-in margin (ruler, clip
  drag, section drag/resize, context menu, double-click)
- formatBeat returns 0-indexed bar
- Sub-tab ruler has leadInMargin=0 (no lead-in in clip editor)
This commit is contained in:
2026-07-25 10:25:30 +07:00
parent ff44fd4b8d
commit 8372ee1b98
3 changed files with 87 additions and 64 deletions
+63 -46
View File
@@ -55,10 +55,10 @@ const formatTimeSimple = secs => {
return `${secs.toFixed(2)}s`; return `${secs.toFixed(2)}s`;
}; };
const formatBeat = (secs, bpmVal) => { const formatBeat = (secs, bpmVal) => {
if (isNaN(secs) || secs < 0) return "1.1.1"; if (isNaN(secs) || secs < 0) return "0.1.1";
const beatDuration = 60 / bpmVal; const beatDuration = 60 / bpmVal;
const barDuration = beatDuration * 4; const barDuration = beatDuration * 4;
const bar = Math.floor(secs / barDuration) + 1; const bar = Math.floor(secs / barDuration);
const beat = Math.floor((secs % barDuration) / beatDuration) + 1; const beat = Math.floor((secs % barDuration) / beatDuration) + 1;
const sub = Math.floor((secs % beatDuration) / (beatDuration / 4)) + 1; const sub = Math.floor((secs % beatDuration) / (beatDuration / 4)) + 1;
return `${bar}.${beat}.${sub}`; return `${bar}.${beat}.${sub}`;
@@ -418,6 +418,7 @@ const WaveformLane = ({
}) => { }) => {
const canvasRef = useRef(null); const canvasRef = useRef(null);
const drawWidth = Math.min(timelineWidth, viewportWidth); const drawWidth = Math.min(timelineWidth, viewportWidth);
const leadInMargin = (60.0 / (parseFloat(bpm) || 120)) * 4;
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas) return;
@@ -440,9 +441,11 @@ const WaveformLane = ({
ctx.lineWidth = 1; ctx.lineWidth = 1;
const beatDuration = (60.0 / (parseFloat(bpm) || 120)); const beatDuration = (60.0 / (parseFloat(bpm) || 120));
const barDuration = beatDuration * 4; const barDuration = beatDuration * 4;
const leadIn = barDuration;
const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
const PADDING_LEFT = barDuration; // 1-bar margin on left const PADDING_LEFT = barDuration; // 1-bar margin on left
const tStart = scrollLeftVal / zoom - PADDING_LEFT; const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
const tEnd = (scrollLeftVal + drawWidth) / zoom + PADDING_LEFT; const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + PADDING_LEFT;
const firstBeat = Math.floor(tStart / beatDuration) * beatDuration; const firstBeat = Math.floor(tStart / beatDuration) * beatDuration;
let snapDivisor = 1; let snapDivisor = 1;
if (snapValue && snapValue !== 'free') { if (snapValue && snapValue !== 'free') {
@@ -452,7 +455,7 @@ const WaveformLane = ({
const beatNum = Math.floor(t / beatDuration) + 1; const beatNum = Math.floor(t / beatDuration) + 1;
const isBar = beatNum % 4 === 1; const isBar = beatNum % 4 === 1;
const localX = (t - tStart) * zoom; const localX = (t - tStart) * zoom;
if (localX < -200 || localX > drawWidth + 200) continue; if (localX < -CLIP_BUFFER || localX > drawWidth + CLIP_BUFFER) continue;
ctx.strokeStyle = isBar ? 'rgba(255, 255, 255, 0.12)' : 'rgba(255, 255, 255, 0.04)'; ctx.strokeStyle = isBar ? 'rgba(255, 255, 255, 0.12)' : 'rgba(255, 255, 255, 0.04)';
ctx.lineWidth = isBar ? 1.2 : 0.5; ctx.lineWidth = isBar ? 1.2 : 0.5;
ctx.beginPath(); ctx.beginPath();
@@ -463,7 +466,7 @@ const WaveformLane = ({
ctx.fillStyle = 'rgba(255, 255, 255, 0.15)'; ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
ctx.font = 'bold 7px Inter, sans-serif'; ctx.font = 'bold 7px Inter, sans-serif';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillText(`${Math.ceil(beatNum / 4)}`, localX + 2, 10); ctx.fillText(`${Math.floor((beatNum - 1) / 4)}`, localX + 2, 10);
} }
} }
@@ -832,7 +835,7 @@ const WaveformLane = ({
if (!canvasRef.current) return; if (!canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left + (scrollLeft || 0); const x = e.clientX - rect.left + (scrollLeft || 0);
const time = x / zoom; const time = Math.max(0, x / zoom - leadInMargin);
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
id: 'default', id: 'default',
buffer: track.buffer, buffer: track.buffer,
@@ -944,7 +947,7 @@ const WaveformLane = ({
if (e.button === 2) return; if (e.button === 2) return;
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left + (scrollLeft || 0); const x = e.clientX - rect.left + (scrollLeft || 0);
const time = Math.max(0, x / zoom); const time = Math.max(0, x / zoom - leadInMargin);
onSelectTrack(track.id); onSelectTrack(track.id);
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
id: 'default', id: 'default',
@@ -1132,7 +1135,7 @@ const WaveformLane = ({
onDoubleClick: e => { onDoubleClick: e => {
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left + (scrollLeft || 0); const x = e.clientX - rect.left + (scrollLeft || 0);
const time = Math.max(0, x / zoom); const time = Math.max(0, x / zoom - leadInMargin);
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
id: 'default', id: 'default',
buffer: track.buffer, buffer: track.buffer,
@@ -1179,7 +1182,7 @@ const WaveformLane = ({
onSelectTrack(track.id); onSelectTrack(track.id);
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left + (scrollLeft || 0); const x = e.clientX - rect.left + (scrollLeft || 0);
const time = Math.max(0, x / zoom); const time = Math.max(0, x / zoom - leadInMargin);
// Detect section under cursor // Detect section under cursor
const secList = track.sections || []; const secList = track.sections || [];
let hitSectionId = null; let hitSectionId = null;
@@ -1192,7 +1195,8 @@ const WaveformLane = ({
}; };
const TimelineRuler = ({ const TimelineRuler = ({
bpm, zoom, timelineWidth, viewportWidth, bpm, zoom, timelineWidth, viewportWidth,
onPlayheadSet, snapValue, onRulerMouseDown, scrollLeft, canvasRedrawCount onPlayheadSet, snapValue, onRulerMouseDown, scrollLeft, canvasRedrawCount,
leadInMargin: propLeadIn
}) => { }) => {
const canvasRef = useRef(null); const canvasRef = useRef(null);
const RULER_HEIGHT = 48; const RULER_HEIGHT = 48;
@@ -1220,15 +1224,17 @@ const TimelineRuler = ({
ctx.stroke(); ctx.stroke();
const beatDuration = 60 / bpm; const beatDuration = 60 / bpm;
const barDuration = beatDuration * 4; const barDuration = beatDuration * 4;
const leadIn = propLeadIn !== undefined ? propLeadIn : barDuration;
const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
const PADDING_LEFT = barDuration; const PADDING_LEFT = barDuration;
const tStart = scrollLeftVal / zoom - PADDING_LEFT; const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
const tEnd = (scrollLeftVal + drawWidth) / zoom + PADDING_LEFT; const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + PADDING_LEFT;
const firstBeat = Math.floor(tStart / beatDuration) * beatDuration; const firstBeat = Math.floor(tStart / beatDuration) * beatDuration;
for (let t = firstBeat; t <= tEnd; t += beatDuration) { for (let t = firstBeat; t <= tEnd; t += beatDuration) {
const beatNum = Math.floor(t / beatDuration) + 1; const beatNum = Math.floor(t / beatDuration) + 1;
const isBar = beatNum % 4 === 1; const isBar = beatNum % 4 === 1;
const localX = (t - tStart) * zoom; const localX = (t - tStart) * zoom;
if (localX < -200 || localX > drawWidth + 200) continue; if (localX < -CLIP_BUFFER || localX > drawWidth + CLIP_BUFFER) continue;
if (isBar) { if (isBar) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.lineWidth = 1.5; ctx.lineWidth = 1.5;
@@ -1239,7 +1245,7 @@ const TimelineRuler = ({
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)'; ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
ctx.font = 'bold 10px Inter, sans-serif'; ctx.font = 'bold 10px Inter, sans-serif';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillText(`${Math.ceil(beatNum / 4)}`, localX + 4, 13); ctx.fillText(`${Math.floor((beatNum - 1) / 4)}`, localX + 4, 13);
} else { } else {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
ctx.lineWidth = 0.8; ctx.lineWidth = 0.8;
@@ -1248,7 +1254,7 @@ const TimelineRuler = ({
ctx.lineTo(localX, height); ctx.lineTo(localX, height);
ctx.stroke(); ctx.stroke();
if (zoom >= 40) { if (zoom >= 40) {
const bar = Math.ceil(beatNum / 4); const bar = Math.floor((beatNum - 1) / 4);
const beat = ((beatNum - 1) % 4) + 1; const beat = ((beatNum - 1) % 4) + 1;
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.font = '7px Inter, sans-serif'; ctx.font = '7px Inter, sans-serif';
@@ -1292,7 +1298,8 @@ const TimelineRuler = ({
onMouseDown: e => { onMouseDown: e => {
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left + (scrollLeft || 0); const x = e.clientX - rect.left + (scrollLeft || 0);
const time = Math.max(0, x / zoom); const leadIn = propLeadIn !== undefined ? propLeadIn : (60 / bpm) * 4;
const time = Math.max(0, x / zoom - leadIn);
if (e.shiftKey) { e.preventDefault(); e.stopPropagation(); } if (e.shiftKey) { e.preventDefault(); e.stopPropagation(); }
if (onRulerMouseDown) onRulerMouseDown(e); if (onRulerMouseDown) onRulerMouseDown(e);
else onPlayheadSet(time, e.shiftKey); else onPlayheadSet(time, e.shiftKey);
@@ -1348,10 +1355,7 @@ const TempoTrackLane = ({
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = 'bold 9px Inter, sans-serif'; ctx.font = 'bold 9px Inter, sans-serif';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.fillText(`${Math.floor((beatNum - 1) / 4)}`, localX + 3, 11);
ctx.font = 'bold 9px Inter, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`${Math.ceil(beatNum / 4)}`, localX + 3, 11);
} else { } else {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
ctx.lineWidth = 1; ctx.lineWidth = 1;
@@ -1362,7 +1366,7 @@ const TempoTrackLane = ({
} }
ctx.fillStyle = 'rgba(255, 255, 255, 0.35)'; ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
ctx.font = '7px Inter, sans-serif'; ctx.font = '7px Inter, sans-serif';
const bar = Math.ceil(beatNum / 4); const bar = Math.floor((beatNum - 1) / 4);
const beat = ((beatNum - 1) % 4) + 1; const beat = ((beatNum - 1) % 4) + 1;
ctx.fillText(`${bar}:${beat}`, localX + 2, height - 3); ctx.fillText(`${bar}:${beat}`, localX + 2, height - 3);
} }
@@ -6119,8 +6123,8 @@ const App = () => {
const [localSelectionEnd, setLocalSelectionEnd] = useState(null); const [localSelectionEnd, setLocalSelectionEnd] = useState(null);
const [zoom, setZoom] = useState(100); const [zoom, setZoom] = useState(100);
const [isLoopingSelection, setIsLoopingSelection] = useState(false); const [isLoopingSelection, setIsLoopingSelection] = useState(false);
const [beginBar, setBeginBar] = useState(1); const [beginBar, setBeginBar] = useState(0);
const [endBar, setEndBar] = useState(1); const [endBar, setEndBar] = useState(0);
const [numberBar, setNumberBar] = useState(1); const [numberBar, setNumberBar] = useState(1);
const [subTabHeight, setSubTabHeight] = useState(96); const [subTabHeight, setSubTabHeight] = useState(96);
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
@@ -8630,6 +8634,9 @@ const App = () => {
}, []); }, []);
// Computed Values // Computed Values
const leadInMargin = useMemo(() => (60.0 / (parseInt(bpm) || 120)) * 4, [bpm]);
const leadInMarginRef = useRef(leadInMargin);
leadInMarginRef.current = leadInMargin;
const maxDuration = useMemo(() => { const maxDuration = useMemo(() => {
const secPerBar = (60.0 / (parseInt(bpm) || 120)) * 4; const secPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
const cur = activeTracks; const cur = activeTracks;
@@ -8668,14 +8675,14 @@ const App = () => {
return viewportWidth / maxDuration; return viewportWidth / maxDuration;
}, [viewportWidth, maxDuration]); }, [viewportWidth, maxDuration]);
const timelineWidth = useMemo(() => { const timelineWidth = useMemo(() => {
return Math.max(zoom * maxDuration, viewportWidth); return Math.max(zoom * (maxDuration + leadInMargin), viewportWidth);
}, [zoom, maxDuration, viewportWidth]); }, [zoom, maxDuration, viewportWidth, leadInMargin]);
useEffect(() => { useEffect(() => {
if (zoom < minZoom) { if (zoom < minZoom) {
setZoom(minZoom); setZoom(minZoom);
} }
}, [minZoom]); }, [minZoom]);
const playheadLeftPos = useMemo(() => currentTime * zoom, [currentTime, zoom]); const playheadLeftPos = useMemo(() => (currentTime + leadInMargin) * zoom, [currentTime, zoom, leadInMargin]);
const selLeft = useMemo(() => { const selLeft = useMemo(() => {
if (selectionMode === 'local' && localSelectionStart !== null && localSelectionEnd !== null) { if (selectionMode === 'local' && localSelectionStart !== null && localSelectionEnd !== null) {
return Math.min(localSelectionStart, localSelectionEnd); return Math.min(localSelectionStart, localSelectionEnd);
@@ -10023,7 +10030,7 @@ const App = () => {
const rect = wrapper.getBoundingClientRect(); const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft; const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft;
const rawTime = mouseX / zoom; const rawTime = Math.max(0, mouseX / zoom - leadInMargin);
const time = snapValue !== 'free' ? snapTime(rawTime, snapValue, bpm) : rawTime; const time = snapValue !== 'free' ? snapTime(rawTime, snapValue, bpm) : rawTime;
clearLocalSelection(); clearLocalSelection();
setSelectionMode('global'); setSelectionMode('global');
@@ -10055,7 +10062,7 @@ const App = () => {
const rect = wrapper.getBoundingClientRect(); const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft; const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft;
const rawTime = Math.max(0, Math.min(maxDuration, mouseX / zoom)); const rawTime = Math.max(0, Math.min(maxDuration, mouseX / zoom - leadInMarginRef.current));
const time = snapValueRef.current !== 'free' ? snapTime(rawTime, snapValueRef.current, bpmRef.current) : rawTime; const time = snapValueRef.current !== 'free' ? snapTime(rawTime, snapValueRef.current, bpmRef.current) : rawTime;
const anchor = rulerAnchorRef.current ?? rulerDragStartRef.current ?? time; const anchor = rulerAnchorRef.current ?? rulerDragStartRef.current ?? time;
setSelectionStart(Math.min(anchor, time)); setSelectionStart(Math.min(anchor, time));
@@ -10322,7 +10329,7 @@ const App = () => {
const rect = wrapper.getBoundingClientRect(); const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft; const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft;
const time = mouseX / zoom; const time = Math.max(0, mouseX / zoom - leadInMarginRef.current);
const beatSec = (60.0 / (parseInt(bpmRef.current) || 120)); const beatSec = (60.0 / (parseInt(bpmRef.current) || 120));
const rawStart = Math.max(beatSec, time - drag.clickOffset); const rawStart = Math.max(beatSec, time - drag.clickOffset);
const secPerBar = beatSec * 4; const secPerBar = beatSec * 4;
@@ -10490,7 +10497,7 @@ const App = () => {
const rect = wrapper.getBoundingClientRect(); const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft; const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft;
const time = mouseX / zoom; const time = Math.max(0, mouseX / zoom - leadInMarginRef.current);
const beatSec = (60.0 / (parseInt(bpm) || 120)); const beatSec = (60.0 / (parseInt(bpm) || 120));
const secondsPerBar = beatSec * 4; const secondsPerBar = beatSec * 4;
const marginBar = maxDurationRef.current - secondsPerBar; const marginBar = maxDurationRef.current - secondsPerBar;
@@ -10566,7 +10573,7 @@ const App = () => {
const rect = wrapper.getBoundingClientRect(); const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft; const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft;
const time = mouseX / zoom; const time = Math.max(0, mouseX / zoom - leadInMarginRef.current);
updateActiveTracks(prev => prev.map(t => { updateActiveTracks(prev => prev.map(t => {
if (t.id !== resize.trackId) return t; if (t.id !== resize.trackId) return t;
const items = resize.itemType === 'section' ? [...(t.sections || [])] : [...(t.midiItems || [])]; const items = resize.itemType === 'section' ? [...(t.sections || [])] : [...(t.midiItems || [])];
@@ -13964,13 +13971,13 @@ const App = () => {
className: "text-[14px] text-zinc-500 font-bold" className: "text-[14px] text-zinc-500 font-bold"
}, "Bars:"), /*#__PURE__*/React.createElement("input", { }, "Bars:"), /*#__PURE__*/React.createElement("input", {
type: "number", type: "number",
min: "1", min: "0",
value: beginBar, value: beginBar,
onChange: e => { onChange: e => {
const b = parseInt(e.target.value) || 1; const b = parseInt(e.target.value) || 0;
setBeginBar(b); setBeginBar(b);
const beatDuration = 60 / parseInt(bpm || 120); const beatDuration = 60 / parseInt(bpm || 120);
const t = (b - 1) * beatDuration * 4; const t = b * beatDuration * 4;
clearLocalSelection(); clearLocalSelection();
setSelectionMode('global'); setSelectionMode('global');
setSelectionStart(t); setSelectionStart(t);
@@ -13981,13 +13988,13 @@ const App = () => {
className: "text-[14px] text-zinc-500" className: "text-[14px] text-zinc-500"
}, "-"), /*#__PURE__*/React.createElement("input", { }, "-"), /*#__PURE__*/React.createElement("input", {
type: "number", type: "number",
min: "1", min: "0",
value: endBar, value: endBar,
onChange: e => { onChange: e => {
const b = parseInt(e.target.value) || 1; const b = parseInt(e.target.value) || 0;
setEndBar(b); setEndBar(b);
const beatDuration = 60 / parseInt(bpm || 120); const beatDuration = 60 / parseInt(bpm || 120);
const t = (b - 1) * beatDuration * 4; const t = b * beatDuration * 4;
setSelectionEnd(t + beatDuration * 4); setSelectionEnd(t + beatDuration * 4);
setNumberBar(b - beginBar + 1); setNumberBar(b - beginBar + 1);
}, },
@@ -14412,13 +14419,13 @@ const App = () => {
className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5" className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
}, "Begin Bar"), /*#__PURE__*/React.createElement("input", { }, "Begin Bar"), /*#__PURE__*/React.createElement("input", {
type: "number", type: "number",
min: "1", min: "0",
value: beginBar, value: beginBar,
onChange: e => { onChange: e => {
const b = parseInt(e.target.value) || 1; const b = parseInt(e.target.value) || 0;
setBeginBar(b); setBeginBar(b);
const beatDuration = 60 / parseInt(bpm || 120); const beatDuration = 60 / parseInt(bpm || 120);
const t = (b - 1) * beatDuration * 4; const t = b * beatDuration * 4;
clearLocalSelection(); clearLocalSelection();
setSelectionMode('global'); setSelectionMode('global');
setSelectionStart(t); setSelectionStart(t);
@@ -14429,13 +14436,13 @@ const App = () => {
className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5" className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
}, "End Bar"), /*#__PURE__*/React.createElement("input", { }, "End Bar"), /*#__PURE__*/React.createElement("input", {
type: "number", type: "number",
min: "1", min: "0",
value: endBar, value: endBar,
onChange: e => { onChange: e => {
const b = parseInt(e.target.value) || 1; const b = parseInt(e.target.value) || 0;
setEndBar(b); setEndBar(b);
const beatDuration = 60 / parseInt(bpm || 120); const beatDuration = 60 / parseInt(bpm || 120);
const t = (b - 1) * beatDuration * 4; const t = b * beatDuration * 4;
setSelectionEnd(t + beatDuration * 4); setSelectionEnd(t + beatDuration * 4);
setNumberBar(b - beginBar + 1); setNumberBar(b - beginBar + 1);
}, },
@@ -15467,9 +15474,19 @@ const App = () => {
viewportWidth: viewportWidth, viewportWidth: viewportWidth,
onPlayheadSet: setCurrentTime, onPlayheadSet: setCurrentTime,
snapValue: snapValue, snapValue: snapValue,
onRulerMouseDown: handleRulerMouseDown, onRulerMouseDown: e => {
const wrapper = timelineWrapperRef.current;
if (!wrapper) return;
const rect = wrapper.getBoundingClientRect();
const sl = wrapper.scrollLeft;
const raw = Math.max(0, (e.clientX - rect.left + sl) / zoom);
const t = snapValue !== 'free' ? snapTime(raw, snapValue, bpm) : raw;
if (e.shiftKey) { e.preventDefault(); e.stopPropagation(); }
handlePlayheadSet(t);
},
scrollLeft: scrollLeft, scrollLeft: scrollLeft,
canvasRedrawCount: canvasRedrawCount canvasRedrawCount: canvasRedrawCount,
leadInMargin: 0
}), /*#__PURE__*/React.createElement("div", { }), /*#__PURE__*/React.createElement("div", {
className: "flex-1 flex flex-col relative bg-[#111111] min-h-full" className: "flex-1 flex flex-col relative bg-[#111111] min-h-full"
}, vTrack && /*#__PURE__*/React.createElement("div", { }, vTrack && /*#__PURE__*/React.createElement("div", {
File diff suppressed because one or more lines are too long
+6
View File
@@ -79,3 +79,9 @@
- **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes. - **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.
---