feat(CAD): 휠 = 늘 줌(커서 중심) · 휠 처리를 input-wheel.ts 로 떼기(153-3)

- 마우스 · 트랙패드 판정 지움 — 확대율 100 % 아닌 화면에서 휠이 팬으로 먹히던 것
- 트랙패드 두 손가락 팬 없어짐 · 팬 = 가운데 단추 끌기
- vitest input-wheel.test.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
This commit is contained in:
2026-10-09 19:06:37 +09:00
co-authored by Claude Opus 5.5
parent f70345e704
commit 7cad25bb7f
3 changed files with 61 additions and 43 deletions
@@ -1,13 +1,7 @@
import { Point } from '@flatten-js/core';
import { compact, round } from 'es-toolkit';
import { Actor } from 'xstate';
import {
PINCH_ZOOM_EXPONENT,
SNAP_POINT_DISTANCE,
WHEEL_LINE_PX,
WHEEL_PAGE_PX,
WHEEL_ZOOM_EXPONENT,
} from '../App.consts.ts';
import { SNAP_POINT_DISTANCE } from '../App.consts.ts';
import { MouseButton } from '../App.types.ts';
import {
describeCommand,
@@ -66,6 +60,7 @@ import {
isPlainSpace,
} from './ime-sink.ts';
import { drawInputField } from './input-field-draw.ts';
import { wheelZoom } from './input-wheel.ts';
import {
ABSOLUTE_POINT_REGEXP,
ABSOLUTE_POLAR_REGEXP,
@@ -277,30 +272,8 @@ export class InputController {
public handleMouseWheel(evt: WheelEvent) {
// 확대는 도면만 — 트랙패드 핀치(ctrl+휠)가 브라우저 전체를 키우던 것을 막는다.
evt.preventDefault();
const drawController = getScreenCanvasDrawController();
// deltaY 단위를 픽셀로 맞춘다 — 브라우저에 따라 줄(1)·쪽(2) 단위로 오기도 한다.
const unitPx = evt.deltaMode === 1 ? WHEEL_LINE_PX : evt.deltaMode === 2 ? WHEEL_PAGE_PX : 1;
const deltaX = evt.deltaX * unitPx;
const deltaY = evt.deltaY * unitPx;
// 핀치(ctrl+휠)는 기기와 무관하게 확대.
if (evt.ctrlKey) {
if (deltaY !== 0) drawController.zoomScreen(-deltaY, PINCH_ZOOM_EXPONENT);
return;
}
// 트랙패드 두 손가락 이동은 팬 — 문서를 스크롤하듯 화면이 손가락 반대로 간다.
if (!isMouseWheel(evt)) {
drawController.panScreen(-deltaX, deltaY);
return;
}
if (deltaY === 0) {
return; // We can't zoom by zero delta
}
// Aislo: wheel direction is inverted on purpose - pulling the wheel zooms in, pushing
// zooms out, matching the B04/B05 maps and the B06 cross sections (2026-08-02).
drawController.zoomScreen(-deltaY, WHEEL_ZOOM_EXPONENT);
const zoom = wheelZoom(evt);
if (zoom) getScreenCanvasDrawController().zoomScreen(zoom.delta, zoom.exponent);
}
public handleMouseDown(evt: MouseEvent) {
@@ -643,15 +616,3 @@ export class InputController {
});
}
}
/**
* 마우스 휠인지 트랙패드인지 가른다. 마우스 휠은 한 칸이 정해진 크기(Chrome 100px,
* 일부 브라우저 120px)로 딱 떨어지고 가로 델타가 없다. 트랙패드는 손가락이 움직인
* 만큼 잔 델타를 보내고 가로 델타도 함께 온다. 줄·쪽 단위(deltaMode≠0)는 휠뿐이다.
*/
function isMouseWheel(evt: WheelEvent): boolean {
if (evt.deltaMode !== 0) return true;
if (evt.deltaX !== 0) return false;
const step = Math.abs(evt.deltaY);
return step !== 0 && (step % 100 === 0 || step % 120 === 0);
}
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import {
PINCH_ZOOM_EXPONENT,
WHEEL_LINE_PX,
WHEEL_PAGE_PX,
WHEEL_ZOOM_EXPONENT,
} from '../App.consts.ts';
import { wheelZoom } from './input-wheel.ts';
const wheel = (deltaY: number, deltaMode = 0, ctrlKey = false) =>
wheelZoom({ deltaY, deltaMode, ctrlKey });
describe('CAD 휠 = 늘 줌(153-3)', () => {
it('100 · 120 배수가 아닌 델타(확대율 90 % · 110 % · 트랙패드 잔 델타)도 줌', () => {
for (const deltaY of [100, 120, 111.1, 90.9, 4, -100, -111.1, -4]) {
expect(wheel(deltaY)).toEqual({ delta: -deltaY, exponent: WHEEL_ZOOM_EXPONENT });
}
});
it('줄 · 쪽 단위는 픽셀로 환산', () => {
expect(wheel(3, 1)).toEqual({ delta: -3 * WHEEL_LINE_PX, exponent: WHEEL_ZOOM_EXPONENT });
expect(wheel(-1, 2)).toEqual({ delta: WHEEL_PAGE_PX, exponent: WHEEL_ZOOM_EXPONENT });
});
it('ctrl+휠(핀치)은 핀치 지수', () => {
expect(wheel(-7, 0, true)).toEqual({ delta: 7, exponent: PINCH_ZOOM_EXPONENT });
});
it('델타 0 은 안 움직임', () => {
expect(wheel(0)).toBeNull();
expect(wheel(0, 0, true)).toBeNull();
});
});
@@ -0,0 +1,24 @@
import {
PINCH_ZOOM_EXPONENT,
WHEEL_LINE_PX,
WHEEL_PAGE_PX,
WHEEL_ZOOM_EXPONENT,
} from '../App.consts.ts';
/**
* 휠 한 번 → 화면 확대(`zoomScreen` 인자). 휠은 늘 줌 · 커서 자리 고정(153-3) — 마우스 · 트랙패드를
* 가르지 않는다(옛 판정은 확대율 ≠ 100 % 등에서 휠을 팬으로 오판). 팬 = 가운데 단추 끌기.
* null = 델타 0 이라 안 움직임.
*/
export function wheelZoom(
evt: Pick<WheelEvent, 'deltaMode' | 'deltaY' | 'ctrlKey'>
): { delta: number; exponent: number } | null {
// deltaY 단위를 픽셀로 맞춘다 — 브라우저에 따라 줄(1)·쪽(2) 단위로 오기도 한다.
const unitPx = evt.deltaMode === 1 ? WHEEL_LINE_PX : evt.deltaMode === 2 ? WHEEL_PAGE_PX : 1;
const deltaY = evt.deltaY * unitPx;
if (deltaY === 0) return null;
// Aislo: wheel direction is inverted on purpose - pulling the wheel zooms in, pushing
// zooms out, matching the B04/B05 maps and the B06 cross sections (2026-08-02).
// 핀치(ctrl+휠)는 델타가 잘아 지수만 크게.
return { delta: -deltaY, exponent: evt.ctrlKey ? PINCH_ZOOM_EXPONENT : WHEEL_ZOOM_EXPONENT };
}