diff --git a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.test.ts b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.test.ts index e1e6e1fbe..4e1418c52 100644 --- a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.test.ts +++ b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.test.ts @@ -3,6 +3,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { LineEntity } from '../entities/LineEntity'; import type { Layer } from '../App.types'; import { setEntities, setLayers } from '../state'; +import { drawInputField } from '../inputController/input-field-draw'; import { ScreenCanvasDrawController } from './screenCanvas.drawController'; /** translate·drawImage 인자만 기록하는 최소 2d 컨텍스트. */ @@ -12,12 +13,19 @@ function recordingContext() { rotate: [], image: [], }; + const rects: number[][] = []; + const texts: { label: string; at: number[] }[] = []; const context = { translate: (x: number, y: number) => calls.translate.push([x, y]), rotate: (a: number) => calls.rotate.push(a), drawImage: (_img: unknown, ...rest: number[]) => calls.image.push(...rest), + save: () => {}, + restore: () => {}, + fillRect: (...rect: number[]) => rects.push(rect), + fillText: (label: string) => texts.push({ label, at: calls.translate.at(-1) ?? [] }), + measureText: (label: string) => ({ width: label.length * 7 }), } as unknown as CanvasRenderingContext2D; - return { context, calls }; + return { context, calls, rects, texts }; } describe('ScreenCanvasDrawController.drawImage', () => { @@ -71,3 +79,45 @@ describe('ScreenCanvasDrawController.zoomToFitScreen', () => { } }); }); + +describe('커서 입력창(drawInputField)', () => { + it('글 중심이 칸 안 · 긴 줄에 맞춰 칸 폭이 늘고 목록도 바탕 안', () => { + const g = globalThis as Record; + g.document = { documentElement: {} }; + g.getComputedStyle = () => ({ getPropertyValue: () => '' }); + const { context, rects, texts } = recordingContext(); + const controller = new ScreenCanvasDrawController(context); + controller.setCanvasSize(new Point(800, 600)); + controller.setScreenMouseLocation(new Point(100, 300)); + const lines = ['LINE (L) 선', 'LINETYPE (LT) 선종류 — 아주 긴 이름의 명령 후보 줄']; + drawInputField(controller, 'LIN', lines); + + const [border, box, divider] = rects; // [x, 캔버스 y(위 변), w, h] + expect(border).toEqual([119, 319, box[2] + 2, box[3] + 2]); + expect(box[0]).toBe(120); + expect(box[1]).toBe(600 - 280); // 커서 아래 20px + expect(box[3]).toBe(20 * 3); // 입력 줄 + 목록 두 줄 + expect(box[2]).toBe(Math.ceil(lines[1].length * 7) + 8); // 가장 긴 줄 + 여백 + expect(divider).toEqual([120, 339, box[2], 1]); // 입력 줄 아래 끝 한 줄 + + expect(texts.map((text) => text.label)).toEqual(['LIN', ...lines]); + texts.forEach((text, row) => { + const [x, y] = text.at; + expect(x).toBe(124); + // 글 중심 = 그 줄 세로 가운데 · 12px 글 전체가 칸 안 + expect(y).toBe(box[1] + 20 * row + 10); + expect(y - 6).toBeGreaterThanOrEqual(box[1]); + expect(y + 6).toBeLessThanOrEqual(box[1] + box[3]); + }); + }); + + it('빈 입력 · 짧은 글은 최소 폭 150 · 한 줄', () => { + const { context, rects } = recordingContext(); + const controller = new ScreenCanvasDrawController(context); + controller.setCanvasSize(new Point(800, 600)); + controller.setScreenMouseLocation(new Point(100, 300)); + drawInputField(controller, '', []); + expect(rects).toHaveLength(2); + expect(rects[1]).toEqual([120, 320, 150, 20]); + }); +}); diff --git a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts index 54b115088..d48309e2e 100644 --- a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts +++ b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts @@ -525,6 +525,15 @@ export class ScreenCanvasDrawController implements DrawController { }); } + /** 화면 글 폭(px) — drawTextScreen 과 같은 글꼴로 잼 */ + public measureTextScreen(label: string, fontSize: number): number { + this.context.save(); + this.context.font = `${fontSize}px ${DEFAULT_TEXT_OPTIONS.fontFamily}`; + const width = this.context.measureText(label).width; + this.context.restore(); + return width; + } + /** * Draw some text at the base location * The direction vector points from the bottom of the first letter towards the bottom of the last letter indicate the rotation of the text diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts b/B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts index f7e8a81c8..ab2d70848 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts @@ -27,7 +27,13 @@ export function inputFieldColors(read: (name: string, fallback: string) => strin }; } -/** 입력 글 칸 + 그 아래 안내 · 좌표 · 명령 후보 줄 */ +/** 글 좌우 여백(px) */ +const INPUT_FIELD_PADDING = 4; + +/** + * 입력 글 칸 + 그 아래 안내 · 좌표 · 명령 후보 줄 — 바탕 한 장(명령줄 후보 상자 꼴). + * 줄마다 높이 CANVAS_INPUT_FIELD_HEIGHT · 글 중심 = 줄 세로 가운데 · 폭 = max(최소 폭, 가장 긴 줄 + 여백). + */ export function drawInputField( drawController: ScreenCanvasDrawController, input: string, @@ -35,33 +41,34 @@ export function drawInputField( ): 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; - // 화면 좌표는 위가 + — fillRectScreen 의 y 는 칸 아래 변 - const bottom = mouse.y - CANVAS_INPUT_FIELD_MOUSE_OFFSET - CANVAS_INPUT_FIELD_HEIGHT; - const width = CANVAS_INPUT_FIELD_WIDTH; + // 화면 좌표는 위가 + — 칸 위 변이 커서 아래 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, - bottom - 1, + top - height - 1, width + 2, - CANVAS_INPUT_FIELD_HEIGHT + 2, + height + 2, colors.border, true ); - drawController.fillRectScreen( - left, - bottom, - width, - CANVAS_INPUT_FIELD_HEIGHT, - colors.background, - true - ); - const textOptions = { - textAlign: 'left' as const, - fontSize: CANVAS_INPUT_FIELD_FONT_SIZE, - rawColor: 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 + 2, bottom - 2), { + drawController.drawTextScreen(input, new Point(left + INPUT_FIELD_PADDING, rowCenter(0)), { ...textOptions, textColor: colors.text, }); @@ -69,11 +76,11 @@ export function drawInputField( lines.forEach((line, index) => { drawController.drawTextScreen( line, - new Point( - left + 2, - bottom - CANVAS_INPUT_FIELD_HEIGHT - 2 - index * CANVAS_INPUT_FIELD_HEIGHT - ), - { ...textOptions, textColor: colors.dimText } + new Point(left + INPUT_FIELD_PADDING, rowCenter(index + 1)), + { + ...textOptions, + textColor: colors.dimText, + } ); }); }