fix(B07-CAD): 커서 입력창 글이 칸 안 — 줄 세로 가운데 · 칸 폭 = 가장 긴 줄 · 후보 목록도 바탕 안(97-2)
- 글 중심 = 줄 세로 가운데(옛 칸 아래 변 2px 밑) - 칸 폭 = max(150, 가장 긴 줄 글 폭 + 여백 8) · measureTextScreen(measureText 감쌈) - 안내 · 좌표 · 명령 후보(최대 6줄)도 바탕 한 장 안 · 입력 줄 아래 가는 줄 - 그리기 시험: 글 중심 · 12px 글 전체가 칸 안 · 폭 · 최소 폭 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
+51
-1
@@ -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<string, unknown>;
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user