/** * 커서 따라다니는 입력창 — 캔버스에 직접 그림(`input-controller.ts` draw 가 부름). * 칸 · 글은 테마 토큰 색을 그대로 칠함(자동색 뒤집기 건너뜀) — 명령줄 후보 상자(`App.css` * `.cad-command-suggestions`)와 같은 색. */ import { Point } from '@flatten-js/core'; import { CANVAS_INPUT_FIELD_BACKGROUND_COLOR, CANVAS_INPUT_FIELD_BORDER_COLOR, CANVAS_INPUT_FIELD_FONT_SIZE, CANVAS_INPUT_FIELD_HEIGHT, CANVAS_INPUT_FIELD_INSTRUCTION_TEXT_COLOR, CANVAS_INPUT_FIELD_MOUSE_OFFSET, CANVAS_INPUT_FIELD_TEXT_COLOR, CANVAS_INPUT_FIELD_WIDTH, } from '../App.consts.ts'; import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController.ts'; import { themeColor } from '../theme.ts'; /** 입력창 색 — 밝음 = 흰 칸 + 테두리 · 어두움 = 짙은 보라 칸 · 글 = CAD 글 색 */ export function inputFieldColors(read: (name: string, fallback: string) => string = themeColor) { return { background: read('--cad-chrome-raised', CANVAS_INPUT_FIELD_BACKGROUND_COLOR), border: read('--cad-line', CANVAS_INPUT_FIELD_BORDER_COLOR), text: read('--cad-text', CANVAS_INPUT_FIELD_TEXT_COLOR), dimText: read('--cad-text-dim', CANVAS_INPUT_FIELD_INSTRUCTION_TEXT_COLOR), }; } /** 글 좌우 여백(px) */ const INPUT_FIELD_PADDING = 4; /** * 입력 글 칸 + 그 아래 안내 · 좌표 · 명령 후보 줄 — 바탕 한 장(명령줄 후보 상자 꼴). * 줄마다 높이 CANVAS_INPUT_FIELD_HEIGHT · 글 중심 = 줄 세로 가운데 · 폭 = max(최소 폭, 가장 긴 줄 + 여백). */ export function drawInputField( drawController: ScreenCanvasDrawController, input: string, lines: string[] ): void { const mouse = drawController.getScreenMouseLocation(); const colors = inputFieldColors(); const rowHeight = CANVAS_INPUT_FIELD_HEIGHT; const fontSize = CANVAS_INPUT_FIELD_FONT_SIZE; const left = mouse.x + CANVAS_INPUT_FIELD_MOUSE_OFFSET; // 화면 좌표는 위가 + — 칸 위 변이 커서 아래 OFFSET · fillRectScreen 의 y 는 아래 변 const top = mouse.y - CANVAS_INPUT_FIELD_MOUSE_OFFSET; const height = rowHeight * (1 + lines.length); const textWidth = Math.max( 0, ...[input, ...lines].map((text) => drawController.measureTextScreen(text, fontSize)) ); const width = Math.max(CANVAS_INPUT_FIELD_WIDTH, Math.ceil(textWidth) + INPUT_FIELD_PADDING * 2); drawController.fillRectScreen( left - 1, top - height - 1, width + 2, height + 2, colors.border, true ); drawController.fillRectScreen(left, top - height, width, height, colors.background, true); if (lines.length) { // 입력 줄과 목록 사이 가는 줄 drawController.fillRectScreen(left, top - rowHeight, width, 1, colors.border, true); } const textOptions = { textAlign: 'left' as const, fontSize, rawColor: true }; const rowCenter = (row: number) => top - rowHeight * row - rowHeight / 2; if (input) { drawController.drawTextScreen(input, new Point(left + INPUT_FIELD_PADDING, rowCenter(0)), { ...textOptions, textColor: colors.text, }); } lines.forEach((line, index) => { drawController.drawTextScreen( line, new Point(left + INPUT_FIELD_PADDING, rowCenter(index + 1)), { ...textOptions, textColor: colors.dimText, } ); }); } /** * 동적 입력 두 칸 — `[길이 100 🔒] ▸[각도 30_]`. 잠근 칸은 🔒 · 지금 칸은 ▸ + 친 글 · * 나머지는 기준점→커서 실측(각도 = 동쪽 0° 반시계 +). */ export function dynamicInputLine( base: Point, mouse: Point, field: 'length' | 'angle', typing: string, lockedLength: number | null, lockedDegrees: number | null ): string { const live = { length: Math.hypot(mouse.x - base.x, mouse.y - base.y), angle: ((Math.atan2(mouse.y - base.y, mouse.x - base.x) * 180) / Math.PI + 360) % 360, }; const cell = (name: 'length' | 'angle', label: string, locked: number | null) => { const active = field === name; const value = active && typing ? `${typing}_` : (locked ?? Number(live[name].toFixed(2))); return `${active ? '▸' : ' '}[${label} ${value}${locked !== null ? ' 🔒' : ''}]`; }; return `${cell('length', '길이', lockedLength)} ${cell('angle', '각도', lockedDegrees)}`; }