Files
Aislo/B07_DesignDetail/openwebcad/src/inputController/ime-sink.ts
T
eomsangdonandClaude Opus 5.5 456c11aa5d feat(B07-CAD): Space = Enter(오토캐드) · 빈 Enter · Space = 마지막 명령 반복(97-3)
- 캔버스 · 명령줄에서 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
2026-10-08 14:02:30 +09:00

83 lines
3.5 KiB
TypeScript

/**
* 한글 IME 받개 — 캔버스에는 글 칸이 없어 조합(compositionstart/update/end)이 생기지 않는다.
* 문자를 기다리는 단계(TEXT · MTEXT · 표 칸 · 지시선 …)에서만 숨은 input 에 초점을 줘 조합을 받고,
* 끝난 글자를 넘긴다. 명령 단계에서는 초점을 풀어 한글 자판이어도 L · REC 같은 영문 명령이 들어간다.
*/
import { getActiveToolActor } from '../state.ts';
/** 조합 중 키 — 단축키·글자 처리가 먹지 않고 IME 에 넘긴다 */
export const isImeKey = (evt: KeyboardEvent): boolean =>
evt.isComposing || evt.keyCode === 229 || evt.key === 'Process';
/** 활성 도구의 지금 단계가 TEXT_INPUT 을 받는가 */
export function activeToolAcceptsText(): boolean {
const snapshot = getActiveToolActor()?.getSnapshot();
return !!snapshot?.machine?.states?.[snapshot?.value]?.config?.on?.TEXT_INPUT;
}
/** 지금 단계가 자유 글(TEXT · MTEXT · 표 칸 · 이름 …)을 받는가 — Space 가 띄어쓰기(아니면 Enter) */
export function activeToolWantsFreeText(): boolean {
const meta = getActiveToolActor()?.getSnapshot()?.getMeta?.() as
| Record<string, { kind?: string } | undefined>
| undefined;
return Object.values(meta ?? {}).some((stateMeta) => stateMeta?.kind === 'text');
}
/** Enter 로 받을 Space — 수정키(Shift · Ctrl · Alt · Meta: 한/영 전환 등) · 길게 눌러 반복은 아님 */
export const isPlainSpace = (
evt: Pick<KeyboardEvent, 'key' | 'shiftKey' | 'ctrlKey' | 'altKey' | 'metaKey' | 'repeat'>
): boolean =>
evt.key === ' ' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey && !evt.metaKey && !evt.repeat;
export class ImeSink {
/** 조합 중인 글자 — 커서 옆 입력 칸에 미리 보인다 */
public composing = '';
public element: HTMLInputElement | null = null;
constructor(private readonly onCommit: (text: string) => void) {}
public attach() {
const el = document.createElement('input');
el.className = 'cad-ime-sink';
el.tabIndex = -1;
el.autocomplete = 'off';
el.setAttribute('aria-hidden', 'true');
el.style.cssText =
'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;border:0;padding:0;pointer-events:none';
el.addEventListener('compositionupdate', (evt) => this.handleCompositionUpdate(evt.data));
el.addEventListener('compositionend', (evt) => this.handleCompositionEnd(evt.data));
el.addEventListener('input', (evt) => this.handleInput(evt as InputEvent));
document.body.appendChild(el);
this.element = el;
}
public handleCompositionUpdate(data: string) {
this.composing = data ?? '';
}
public handleCompositionEnd(data: string) {
this.composing = '';
if (this.element) this.element.value = '';
if (data) this.onCommit(data);
}
/** 조합 직후 IME 가 바로 넣는 글자(띄어쓰기 · 숫자) — 조합 중 input 은 compositionend 가 맡는다 */
public handleInput(evt: Pick<InputEvent, 'isComposing' | 'data'>) {
if (evt.isComposing || !evt.data) return;
if (this.element) this.element.value = '';
this.onCommit(evt.data);
}
/** 문자를 기다리면 받개에 초점 · 아니면 놓음. 다른 입력 칸(명령줄 · 대화상자)의 초점은 뺏지 않는다 */
public sync(wantsText: boolean) {
const el = this.element;
if (!el) return;
const active = document.activeElement;
if (wantsText && !active?.closest('input, textarea, select, [contenteditable]')) {
el.focus({ preventScroll: true });
} else if (!wantsText && active === el && !this.composing) {
el.blur();
}
}
}