Files
Aislo/B07_DesignDetail/openwebcad/src/inputController/zoom-prompt.ts
T
eomsangdonandClaude Opus 5.5 66279024d8 feat(B07 CAD): 337-2 마-2 ZOOM 덧물음 · Shift+우클릭 스냅 메뉴 · 고르기 ALL/L/P · 그립 종료(X) · ↶↷ 끊고 되돌림 · snapped 캐스트 지움
- ZOOM(Z): 「윈도우 구석 지정, 축척 비율 (nX 또는 nXP) 입력 또는 [전체(A)/범위(E)/이전(P)/윈도우(W)] <실시간>:」 덧물음 · E · A = 범위 · P = 앞 화면 · W · 누름 = 두 구석 윈도우 · 2X = 배율 곱 · 빈 Enter = 실시간 안내 · 중심 · 동적 · 축척 · 객체 = 없음 안내 · 'Z 투명 · ⛶ 단추 = 바로 범위
- 약속 더함: SubPrompt.onText(꼴이 정해지지 않은 글 · nX) — routeTyped 가 덧물음에 먼저 물음 · undefined = 사다리 그대로(브레인 알림)
- Shift+우클릭: 점 물음에서만 한 번 스냅 메뉴(시작점에서 · 두 점 사이의 중간 · 끝점 · 중간점 · 교차점 · 중심 · 사분점 · 수직 · 노드 · 근처점 · 없음) → 고른 이름을 친 것과 같은 길 · Esc · 바깥 누름 = 메뉴만 닫음(ic/snap-menu.ts)
- 고르기 숨은 키워드 ALL · L · P(선택 도구 첫 점 대기 · 쉼 · 명령 안 자식) — 앞 고름에 더함 · 「n개를 찾음, 총 m개」
- 그립 물음 새 꼴 「** 신축 ** 신축점 지정 또는 [종료(X)]:」 · X = 놓음 · 기준점(B) · 복사(C) · 명령 취소(U) 없음 안내
- ↶ ↷ 단추 = undoCommand · redoCommand(끊고 되돌림) · 우클릭 = 명령 중 Enter · 쉴 때 반복(가-1 길 · 시험)
- 시험: mouse-acad 마-2 더함(21)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
2026-10-16 00:11:53 +09:00

159 lines
5.5 KiB
TypeScript

/**
* 보기 바꿈(337 마) — 범위 줌 · 앞 화면(ZOOM 이전(P)) · 휠 두 번 = 범위 · ZOOM 덧물음.
* 앞 화면 기억 = ZOOM · 휠 두 번 · ⛶ 로 바꾼 것만(휠 굴림 · 팬은 안 쌓음) · 오토캐드처럼 열 개.
* ZOOM 은 덧물음 — 도구를 안 바꿔 명령 중 `'Z` 도 같은 길(투명).
*/
import { Point } from '@flatten-js/core';
import type { OptionSpec, PromptSpec } from '../commands/prompt.ts';
import { getScreenCanvasDrawController } from '../state.ts';
import { notice, pushSubPrompt, type SubPrompt, type SubResult } from './prompt-input.ts';
type View = { scale: number; offset: Point };
const MAX_VIEWS = 10;
/** 휠 두 번 — 이 안에 같은 자리(화면 픽셀)를 다시 누르면 */
const DOUBLE_MS = 400;
const DOUBLE_PX = 5;
const views: View[] = [];
let lastMiddle: { time: number; x: number; y: number } | null = null;
/** 지금 화면을 앞 화면으로 적고 바꿈 */
export function changeView(change: () => void): void {
const controller = getScreenCanvasDrawController();
views.push({ scale: controller.getScreenScale(), offset: controller.getScreenOffset() });
if (views.length > MAX_VIEWS) views.shift();
change();
}
/** 범위 줌 — 도면 전체를 화면에 */
export const zoomExtents = (): void =>
changeView(() => getScreenCanvasDrawController().zoomToFitScreen());
/** 앞 화면으로 — 없으면 false */
export function zoomPrevious(): boolean {
const view = views.pop();
if (!view) return false;
const controller = getScreenCanvasDrawController();
controller.setScreenScale(view.scale);
controller.setScreenOffset(view.offset);
return true;
}
/** 가운데 단추 누름 — 두 번째 누름이면 true(그 뒤 다시 처음부터) */
export function isMiddleDouble(point: { x: number; y: number }, time = Date.now()): boolean {
const double =
!!lastMiddle &&
time - lastMiddle.time <= DOUBLE_MS &&
Math.hypot(point.x - lastMiddle.x, point.y - lastMiddle.y) <= DOUBLE_PX;
lastMiddle = double ? null : { time, x: point.x, y: point.y };
return double;
}
/** 배율만 바꿈 — 화면 가운데 자리를 붙잡음 */
function setScaleAboutCenter(scale: number): void {
const controller = getScreenCanvasDrawController();
const size = controller.getCanvasSize();
const offset = controller.getScreenOffset();
const old = controller.getScreenScale();
const cx = offset.x + size.x / 2 / old;
const cy = offset.y + size.y / 2 / old;
controller.setScreenScale(scale);
controller.setScreenOffset(new Point(cx - size.x / 2 / scale, cy - size.y / 2 / scale));
}
/** 윈도우 줌 — 두 구석 상자를 화면에 꽉(종횡비 유지) */
export function zoomWindow(a: Point, b: Point): void {
const controller = getScreenCanvasDrawController();
const size = controller.getCanvasSize();
const width = Math.abs(a.x - b.x);
const height = Math.abs(a.y - b.y);
const scale = Math.min(
width > 0 ? size.x / width : Number.POSITIVE_INFINITY,
height > 0 ? size.y / height : Number.POSITIVE_INFINITY
);
if (!Number.isFinite(scale)) return; // 같은 점 두 번 — 안 바꿈
const cx = (a.x + b.x) / 2;
const cy = (a.y + b.y) / 2;
changeView(() => {
controller.setScreenScale(scale);
controller.setScreenOffset(new Point(cx - size.x / 2 / scale, cy - size.y / 2 / scale));
});
}
const zoomOption = (key: string, kw: string, label: string): OptionSpec => ({
key,
kw,
en: key,
label,
});
/** 오토캐드 ZOOM 첫 줄 — 넣은 넷 · 뺀 넷 · <실시간> */
const ZOOM_PROMPT: PromptSpec = {
text: '윈도우 구석 지정, 축척 비율 (nX 또는 nXP) 입력',
options: [
zoomOption('ALL', 'A', '전체'),
zoomOption('EXTENTS', 'E', '범위'),
zoomOption('PREVIOUS', 'P', '이전'),
zoomOption('WINDOW', 'W', '윈도우'),
],
absent: [
zoomOption('CENTER', 'C', '중심'),
zoomOption('DYNAMIC', 'D', '동적'),
zoomOption('SCALE', 'S', '축척'),
zoomOption('OBJECT', 'O', '객체'),
],
default: { kind: 'enter', label: '실시간' },
};
const FIRST_CORNER: PromptSpec = { text: '첫 번째 구석 지정' };
const OTHER_CORNER: PromptSpec = { text: '반대 구석 지정' };
const SCALE_REGEXP = /^(\d*\.?\d+)\s*XP?$/i;
/** ZOOM — 덧물음 하나가 단계(첫 줄 → 첫 구석 → 반대 구석)를 품음 */
export function startZoomPrompt(): void {
let stage: 'main' | 'first' | 'other' = 'main';
let corner: Point | null = null;
const sub: SubPrompt = {
kind: 'point',
get prompt() {
return stage === 'main' ? ZOOM_PROMPT : stage === 'first' ? FIRST_CORNER : OTHER_CORNER;
},
onPoint(point): SubResult {
if (stage === 'other' && corner) {
zoomWindow(corner, point);
return 'done';
}
corner = point; // 첫 줄에서 누름 = 윈도우 첫 구석(오토캐드)
stage = 'other';
return 'stay';
},
onOption(key): SubResult {
if (key === 'WINDOW') {
stage = 'first';
return 'stay';
}
if (key === 'PREVIOUS') {
if (!zoomPrevious()) notice('앞 화면이 없음.');
return 'done';
}
zoomExtents(); // 전체(A) = 범위(이 CAD 엔 도면 한계 없음)
return 'done';
},
onEnter(): SubResult {
if (stage !== 'main') return 'stay';
notice('실시간 줌은 휠로.');
return 'done';
},
onText(text) {
const match = stage === 'main' ? SCALE_REGEXP.exec(text) : null;
if (!match) return undefined;
const factor = Number.parseFloat(match[1]);
if (factor <= 0) return undefined;
const scale = getScreenCanvasDrawController().getScreenScale() * factor;
changeView(() => setScaleAboutCenter(scale));
return 'done';
},
};
pushSubPrompt(sub);
}