Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getDefaultNoteSettings } from '@src/renderer/defaults';
import type { NoteSettings } from '@src/types/settings/noteSettings';

globalThis.IS_REACT_ACT_ENVIRONMENT = true;

vi.mock('@contexts/useTranslation', () => ({
useTranslation: () => ({
t: (key: string, values?: { value?: number }) =>
values?.value === undefined ? key : `${key}:${values.value}`,
}),
}));
vi.mock('@hooks/usePressAction', () => ({
usePressAction: (action?: () => void) => ({ onClick: action }),
}));
vi.mock('@components/main/common/Checkbox', () => ({
default: ({
checked,
onChange,
}: {
checked: boolean;
onChange: () => void;
}) => (
<button type="button" aria-pressed={checked} onClick={onChange}>
checkbox
</button>
),
}));
vi.mock('@components/main/common/TabSwitch', () => ({
default: ({
tabs,
onTabChange,
}: {
tabs: { id: string; label: string }[];
onTabChange: (id: string) => void;
}) => (
<div>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
data-tab={tab.id}
onClick={() => onTabChange(tab.id)}
>
{tab.label}
</button>
))}
</div>
),
}));
vi.mock('@components/main/Grid/PropertiesPanel/PropertyInputs', () => ({
PropertyRow: ({
label,
children,
}: {
label: React.ReactNode;
children: React.ReactNode;
}) => (
<label data-label={typeof label === 'string' ? label : undefined}>
{label}
{children}
</label>
),
PropertySection: ({ children }: { children: React.ReactNode }) => (
<section>{children}</section>
),
}));
vi.mock('../../Modal', () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));

import NoteSetting from './NoteSetting';

describe('NoteSetting 단노트 정책 안내', () => {
let container: HTMLDivElement;
let root: Root;
let originalResizeObserver: typeof ResizeObserver | undefined;
let originalRequestAnimationFrame: typeof requestAnimationFrame | undefined;
let originalCancelAnimationFrame: typeof cancelAnimationFrame | undefined;

const renderAdvanced = (overrides: Partial<NoteSettings>) => {
const settings = {
...getDefaultNoteSettings(),
...overrides,
};
act(() => {
root.render(<NoteSetting settings={settings} />);
});
act(() => {
(
container.querySelector('[data-tab="advanced"]') as HTMLButtonElement
).click();
});
};

beforeEach(() => {
originalResizeObserver = globalThis.ResizeObserver;
originalRequestAnimationFrame = globalThis.requestAnimationFrame;
originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
globalThis.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
globalThis.requestAnimationFrame = vi.fn(() => 1);
globalThis.cancelAnimationFrame = vi.fn();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});

afterEach(() => {
act(() => root.unmount());
container.remove();
globalThis.ResizeObserver = originalResizeObserver as typeof ResizeObserver;
globalThis.requestAnimationFrame =
originalRequestAnimationFrame as typeof requestAnimationFrame;
globalThis.cancelAnimationFrame =
originalCancelAnimationFrame as typeof cancelAnimationFrame;
vi.restoreAllMocks();
});

it('자동 계산에 트랙 높이로 제한한 최소 길이를 쓴다', () => {
renderAdvanced({
delayedNoteEnabled: true,
speed: 70,
trackHeight: 20,
shortNoteThresholdMs: 300,
shortNoteMinLengthPx: 9999,
});

// 9999px가 아니라 트랙 높이 20px 기준 -> 이동 285.7 + 지연 7.1
expect(container.textContent).toContain('laboratory.keyDelayAuto:293');
});

it('최대 허용 조합의 추천 지연과 30000ms 입력 상한을 노출한다', () => {
renderAdvanced({
delayedNoteEnabled: true,
speed: 70,
trackHeight: 2000,
shortNoteThresholdMs: 2000,
shortNoteMinLengthPx: 1,
});

expect(container.textContent).toContain('laboratory.keyDelayAuto:29564');
const input = container.querySelector(
'[data-label="laboratory.keyDelay"] input',
) as HTMLInputElement;
expect(input.max).toBe('30000');
});
});
38 changes: 26 additions & 12 deletions src/renderer/components/main/Modal/content/settings/NoteSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import {
clampValue,
} from '../../../../../../types/settings/noteSettingsConstraints';
import type { NoteSettings } from '../../../../../../types/settings/noteSettings';
import {
toDisplayDelayMs,
toEffectiveMinLengthPx,
toMinLengthMs,
} from '@utils/core/noteLengthPolicy';

type ConstraintKey = keyof typeof NOTE_SETTINGS_CONSTRAINTS;

Expand Down Expand Up @@ -115,18 +120,27 @@ const NoteSetting = ({
setTabContentHeight((prev) => (prev === nextHeight ? prev : nextHeight));
};

const calculatedDelay = (() => {
const safeSpeed = sanitizeNumericValue(speed, 'speed');
const safeTrackHeight = sanitizeNumericValue(trackHeight, 'trackHeight');
const safeThreshold = sanitizeNumericValue(
shortNoteThresholdMs,
'shortNoteThresholdMs',
);

if (safeSpeed <= 0) return 0;
const travelDelay = Math.round((safeTrackHeight / safeSpeed) * 1000);
return delayedNoteEnabled ? travelDelay + safeThreshold : travelDelay;
})();
const safeSpeed = sanitizeNumericValue(speed, 'speed');
const safeTrackHeight = sanitizeNumericValue(trackHeight, 'trackHeight');
const safeThreshold = sanitizeNumericValue(
shortNoteThresholdMs,
'shortNoteThresholdMs',
);
const safeMinLengthPx = sanitizeNumericValue(
shortNoteMinLengthPx,
'shortNoteMinLengthPx',
);
const effectiveMinPx = toEffectiveMinLengthPx(
safeMinLengthPx,
safeTrackHeight,
);
const minLengthMs = toMinLengthMs(effectiveMinPx, safeSpeed);
const travelDelay = (safeTrackHeight / safeSpeed) * 1000;
// 노트 표시 지연은 오버레이 길이 정책과 같은 식을 써야 키·카운터와 정렬이 맞음
const noteDisplayDelay = toDisplayDelayMs(minLengthMs, safeThreshold);
const calculatedDelay = Math.round(
delayedNoteEnabled ? travelDelay + noteDisplayDelay : travelDelay,
);

const handleAutoCalculate = () => {
const clamped = clampValue(calculatedDelay, 'keyDisplayDelayMs');
Expand Down
18 changes: 13 additions & 5 deletions src/renderer/components/overlay/WebGLTracksOGL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react';
import { Renderer, Camera, Transform, Program, Geometry, Mesh } from 'ogl';
import type { OGLRenderingContext } from 'ogl';
import { animationScheduler } from '@utils/animation/animationScheduler';
import { DEFAULT_NOTE_SETTINGS } from '@constants/overlayDefaults';
import { resolvedFadeValues } from '@src/types/settings/noteSettings';
import type { NoteSettings } from '@src/types/settings/noteSettings';
import {
Expand Down Expand Up @@ -609,11 +610,15 @@ export function WebGLTracksOGL({
depthWrite: false,
uniforms: {
uTime: { value: 0 },
uFlowSpeed: { value: noteSettings.speed || 180 },
uFlowSpeed: {
value: noteSettings.speed || DEFAULT_NOTE_SETTINGS.speed,
},
uScreenHeight: { value: window.innerHeight },
uCanvasBottomDomY: { value: window.innerHeight },
uDomPerPx: { value: 1 },
uTrackHeight: { value: noteSettings.trackHeight || 150 },
uTrackHeight: {
value: noteSettings.trackHeight || DEFAULT_NOTE_SETTINGS.trackHeight,
},
uReverse: { value: noteSettings.reverse ? 1.0 : 0.0 },
uFadeTopPx: { value: resolvedFadeValues(noteSettings).topPx },
uFadeBottomPx: { value: resolvedFadeValues(noteSettings).bottomPx },
Expand Down Expand Up @@ -845,8 +850,10 @@ export function WebGLTracksOGL({
const uniforms = programRef.current.uniforms;
frameLimitRef.current = normalizeFrameLimit(noteSettings?.frameLimit);
resetFrameClock(frameClockRef.current);
uniforms.uFlowSpeed.value = noteSettings.speed || 180;
uniforms.uTrackHeight.value = noteSettings.trackHeight || 150;
uniforms.uFlowSpeed.value =
noteSettings.speed || DEFAULT_NOTE_SETTINGS.speed;
uniforms.uTrackHeight.value =
noteSettings.trackHeight || DEFAULT_NOTE_SETTINGS.trackHeight;
uniforms.uReverse.value = noteSettings.reverse ? 1.0 : 0.0;
const fade = resolvedFadeValues(noteSettings);
uniforms.uFadeTopPx.value = fade.topPx;
Expand Down Expand Up @@ -891,7 +898,8 @@ export function WebGLTracksOGL({
};

const refreshCrop = (): void => {
const trackHeight = noteSettings?.trackHeight || 150;
const trackHeight =
noteSettings?.trackHeight || DEFAULT_NOTE_SETTINGS.trackHeight;
const desired = computeTrackBounds(tracks, trackHeight);
const applied = cropAppliedRef.current;
// 노트가 살아있는 동안엔 확장만 — 축소는 버퍼가 빌 때 반영 (기존 노트 클리핑 방지)
Expand Down
Loading
Loading