- 캔버스 · 명령줄에서 Space(수정키 없음 · 길게 눌러 반복 아님) = Enter · 글 단계(TEXT · MTEXT · 표 칸 · 이름 등)는 띄어쓰기 - 글 단계 판정 = sequence-tool 단계 meta kind 'text'(activeToolWantsFreeText) - 빈 입력 + 선택 도구 대기에서 키보드 Enter · Space = repeatLastCommand · 빈 우클릭은 확정만 - 한글 조합 중 Space · Shift/Ctrl+Space 무시 - 시험 ime-sink: 「L␣」 = LINE · TEXT 띄어쓰기 · 조합 중 · 수정키 · 반복 무시 · 빈 Space · Enter 반복 · 우클릭 반복 안 함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
/** 명령행 — AutoCAD 명령행처럼 기록·입력·자동완성을 담당한다. */
|
|
import { type FC, type FormEvent, useMemo, useRef, useState } from 'react';
|
|
import { describeCommand, matchCommandPrefixes } from '../commands/registry';
|
|
import { getCommandHistory, runCommandInput } from '../commands/run-command';
|
|
import { activeToolWantsFreeText, isPlainSpace } from '../inputController/ime-sink';
|
|
import { getInputController, getLastStateInstructions } from '../state';
|
|
|
|
export const CommandLine: FC = () => {
|
|
const [text, setText] = useState('');
|
|
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const history = getCommandHistory();
|
|
const instruction = getLastStateInstructions() || '명령을 입력하거나 도구를 선택하십시오.';
|
|
|
|
const suggestions = useMemo(() => matchCommandPrefixes(text).slice(0, 8), [text]);
|
|
|
|
const submit = (event: FormEvent) => {
|
|
event.preventDefault();
|
|
const value = text.trim();
|
|
if (!value) {
|
|
// 빈 ENTER는 활성 명령의 확정 신호다 (선택 끝내기·직전 명령 반복)
|
|
getInputController().handleEnterKey(true);
|
|
return;
|
|
}
|
|
// 진행 중인 명령이 문자·숫자를 기다리면 그쪽으로, 아니면 명령으로 해석된다
|
|
getInputController().submitText(value);
|
|
setText('');
|
|
setHistoryIndex(null);
|
|
};
|
|
|
|
const recallHistory = (direction: -1 | 1) => {
|
|
if (!history.length) return;
|
|
const nextIndex =
|
|
historyIndex === null
|
|
? history.length - 1
|
|
: Math.min(history.length - 1, Math.max(0, historyIndex + direction));
|
|
setHistoryIndex(nextIndex);
|
|
setText(history[nextIndex].split(' ')[0]);
|
|
};
|
|
|
|
return (
|
|
<section className="cad-command-area controls">
|
|
<div className="cad-command-history">
|
|
{history.slice(-3).map((line, index) => (
|
|
// biome-ignore lint/suspicious/noArrayIndexKey: 기록은 순서 자체가 식별자다
|
|
<span key={`${line}-${index}`}>{line}</span>
|
|
))}
|
|
</div>
|
|
<form onSubmit={submit}>
|
|
<label htmlFor="cad-command">명령:</label>
|
|
<input
|
|
id="cad-command"
|
|
ref={inputRef}
|
|
value={text}
|
|
onChange={(event) => setText(event.target.value)}
|
|
onKeyDown={(event) => {
|
|
if (event.nativeEvent.isComposing) return; // 한글 조합 중 키는 IME 몫
|
|
if (event.key === ' ' && !activeToolWantsFreeText()) {
|
|
// Space = Enter(오토캐드) · 글 단계는 띄어쓰기 · 수정키 Space 는 그대로(입력칸 몫)
|
|
if (isPlainSpace(event)) {
|
|
event.preventDefault();
|
|
event.currentTarget.form?.requestSubmit();
|
|
} else if (event.repeat) event.preventDefault();
|
|
return;
|
|
}
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
getInputController().handleEscapeKey();
|
|
}
|
|
if (event.key === 'ArrowUp') {
|
|
event.preventDefault();
|
|
recallHistory(-1);
|
|
}
|
|
if (event.key === 'ArrowDown') {
|
|
event.preventDefault();
|
|
recallHistory(1);
|
|
}
|
|
if (event.key === 'Tab' && suggestions.length) {
|
|
event.preventDefault();
|
|
setText(suggestions[0].id);
|
|
}
|
|
}}
|
|
placeholder="명령 입력 (예: L, LINE, REC, MOVE)"
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
/>
|
|
<span className="cad-command-prompt">{instruction}</span>
|
|
</form>
|
|
{text.trim() && suggestions.length > 0 && (
|
|
<div className="cad-command-suggestions">
|
|
{suggestions.map((command) => (
|
|
<button
|
|
type="button"
|
|
key={command.id}
|
|
onClick={() => {
|
|
runCommandInput(command.id);
|
|
setText('');
|
|
inputRef.current?.focus();
|
|
}}
|
|
>
|
|
{describeCommand(command)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
};
|