Merge remote-tracking branch 'origin/sub_desktop_1' into main_laptop_1

This commit is contained in:
2026-10-08 13:55:29 +09:00
7 changed files with 272 additions and 117 deletions
+12 -6
View File
@@ -176,14 +176,20 @@ export const CANVAS_INPUT_FIELD_HEIGHT = 20;
/**
* Canvas input field background color
* 자동색(검정) — 어두운 바탕 검정 칸에 흰 글 · 밝은 바탕 흰 칸에 검정 글(`theme.ts` paintColor)
* 토큰 `--cad-chrome-raised` 못 읽을 때 폴백(어두움 값) — 칸 · 글 · 테두리는 토큰 색을 그대로 칠함
* (자동색 뒤집기 `theme.ts` paintColor 건너뜀 · `inputController/input-field-draw.ts`)
*/
export const CANVAS_INPUT_FIELD_BACKGROUND_COLOR = '#000000';
export const CANVAS_INPUT_FIELD_BACKGROUND_COLOR = '#251f38';
/**
* Canvas input field text color
* Canvas input field border color — 토큰 `--cad-line` 폴백
*/
export const CANVAS_INPUT_FIELD_TEXT_COLOR = '#FFF';
export const CANVAS_INPUT_FIELD_BORDER_COLOR = '#322a45';
/**
* Canvas input field text color — 토큰 `--cad-text` 폴백
*/
export const CANVAS_INPUT_FIELD_TEXT_COLOR = '#e4e0f0';
/**
* Canvas input field background color when text is selected
@@ -201,9 +207,9 @@ export const CANVAS_INPUT_FIELD_SELECTION_TEXT_COLOR = '#000';
export const CANVAS_INPUT_FIELD_FONT_SIZE = 12;
/**
* Canvas input field instruction text color
* Canvas input field instruction text color — 토큰 `--cad-text-dim` 폴백
*/
export const CANVAS_INPUT_FIELD_INSTRUCTION_TEXT_COLOR = '#999';
export const CANVAS_INPUT_FIELD_INSTRUCTION_TEXT_COLOR = '#b3aec6';
/**
* Multiplier to determine the pdf line width from the in application line width
@@ -0,0 +1,47 @@
/** 전체 맞춤 계산 — 맞출 개체 고르기 · 배율 · 화면 원점(`screenCanvas.drawController.ts` zoomToFitScreen 이 부름). */
import { Point } from '@flatten-js/core';
import type { Entity } from '../entities/Entity.ts';
import { getBoundingBoxOfMultipleEntities } from '../helpers/get-bounding-box-of-multiple-entities.ts';
import { getEntities, getPickableEntities } from '../state.ts';
/** M02 가 구조물 도면에 얹는 도각 층 — 서버 `STRUCTURE_FRAME_LAYER` 와 같음 */
const STRUCTURE_FRAME_LAYER_ID = 'm02-structure-frame';
/** 맞출 개체 */
export function fitEntities(): Entity[] {
// 도각·원지반 같은 잠금 도면층은 빼고 설계 콘텐츠에만 맞춘다 (2026-08-31 사용자
// 확정). 도각까지 넣으면 A1 한 장 전체가 잡혀 화면의 40%가 여백으로 간다.
// 구조물 도면(M02·B08)은 얹은 도각(작도 틀)이 있으면 도각 전체에 맞춘다 — 옛 DXF 가 자기 도곽·
// 표제 띠를 품어 범위가 크면 도각이 화면 밖으로 밀린다.
const structureFrame = getEntities().filter((e) => e.layerId === STRUCTURE_FRAME_LAYER_ID);
const pickable = getPickableEntities();
return structureFrame.length ? structureFrame : pickable.length ? pickable : getEntities();
}
/** 개체 범위를 화면 가운데에 놓는 배율 · 화면 원점(월드 좌표) */
export function fitView(entities: Entity[], canvasSize: Point): { zoom: number; offset: Point } {
const boundingBox = getBoundingBoxOfMultipleEntities(entities);
const boundingWidth = boundingBox.maxX - boundingBox.minX;
const boundingHeight = boundingBox.maxY - boundingBox.minY;
// 10% 여백을 남기고 두 축 중 더 빡빡한 쪽에 맞춘다 (종횡비 유지)
const FIT_MARGIN = 0.9;
const scaleX =
boundingWidth > 0 ? (canvasSize.x * FIT_MARGIN) / boundingWidth : Number.POSITIVE_INFINITY;
const scaleY =
boundingHeight > 0 ? (canvasSize.y * FIT_MARGIN) / boundingHeight : Number.POSITIVE_INFINITY;
let zoomLevel = Math.min(scaleX, scaleY);
if (!Number.isFinite(zoomLevel) || zoomLevel <= 0) zoomLevel = 1;
// screen = (world - offset) * zoom 이므로, 도면 중심을 화면 중심에 맞추려면
// offset = worldCenter - (화면 절반 픽셀) / zoom
const worldCenterX = (boundingBox.minX + boundingBox.maxX) / 2;
const worldCenterY = (boundingBox.minY + boundingBox.maxY) / 2;
return {
zoom: zoomLevel,
offset: new Point(
worldCenterX - canvasSize.x / 2 / zoomLevel,
worldCenterY - canvasSize.y / 2 / zoomLevel
),
};
}
@@ -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]);
});
});
@@ -1,12 +1,12 @@
import { Point, type Vector } from '@flatten-js/core';
import { WHEEL_ZOOM_EXPONENT } from '../App.consts';
import { getAngleWithXAxis } from '../helpers/get-angle-with-x-axis.ts';
import { getBoundingBoxOfMultipleEntities } from '../helpers/get-bounding-box-of-multiple-entities.ts';
import { mapNumberRange } from '../helpers/map-number-range.ts';
import { StateVariable } from '../helpers/undo-stack.ts';
import { getEntities, getGridEnabled, getPickableEntities, triggerReactUpdate } from '../state.ts';
import { getGridEnabled, triggerReactUpdate } from '../state.ts';
import { paintColor, themeColor } from '../theme.ts';
import { DEFAULT_TEXT_OPTIONS, type DrawController } from './DrawController';
import { fitEntities, fitView } from './fit-view.ts';
/** 토큰을 못 읽을 때만 쓰는 캔버스 배경 폴백. */
const CANVAS_BACKGROUND_FALLBACK = '#111';
@@ -31,8 +31,6 @@ const CANVAS_BACKGROUND_FALLBACK = '#111';
*
* To convert between the 2 coordinate systems, you need the screenOffset and screenScale
*/
/** M02 가 구조물 도면에 얹는 도각 층 — 서버 `STRUCTURE_FRAME_LAYER` 와 같음 */
const STRUCTURE_FRAME_LAYER_ID = 'm02-structure-frame';
// Batch-mode stroke decimation: skip chain segments shorter than this (screen px)
const BATCH_LOD_PX = 0.5;
// Batch-mode text smaller than this (screen px) is unreadable — skip drawing it
@@ -150,17 +148,7 @@ export class ScreenCanvasDrawController implements DrawController {
* 월드 좌표 offset에 그대로 대입해 중심 배치가 어긋나는 문제가 있었다.)
*/
public zoomToFitScreen() {
// 도각·원지반 같은 잠금 도면층은 빼고 설계 콘텐츠에만 맞춘다 (2026-08-31 사용자
// 확정). 도각까지 넣으면 A1 한 장 전체가 잡혀 화면의 40%가 여백으로 간다.
// 구조물 도면(M02·B08)은 얹은 도각(작도 틀)이 있으면 도각 전체에 맞춘다 — 옛 DXF 가 자기 도곽·
// 표제 띠를 품어 범위가 크면 도각이 화면 밖으로 밀린다.
const structureFrame = getEntities().filter((e) => e.layerId === STRUCTURE_FRAME_LAYER_ID);
const pickable = getPickableEntities();
const entities = structureFrame.length
? structureFrame
: pickable.length
? pickable
: getEntities();
const entities = fitEntities();
if (!entities.length) {
// 빈 도면 — 원점(0,0)을 화면 가운데로. 안 옮기면 기준점 십자가 왼쪽 아래 귀퉁이에 잘린다.
const canvasSize = this.getCanvasSize();
@@ -169,31 +157,9 @@ export class ScreenCanvasDrawController implements DrawController {
this.viewFitted = true;
return;
}
const boundingBox = getBoundingBoxOfMultipleEntities(entities);
const boundingWidth = boundingBox.maxX - boundingBox.minX;
const boundingHeight = boundingBox.maxY - boundingBox.minY;
const canvasSize = this.getCanvasSize();
// 10% 여백을 남기고 두 축 중 더 빡빡한 쪽에 맞춘다 (종횡비 유지)
const FIT_MARGIN = 0.9;
const scaleX =
boundingWidth > 0 ? (canvasSize.x * FIT_MARGIN) / boundingWidth : Number.POSITIVE_INFINITY;
const scaleY =
boundingHeight > 0 ? (canvasSize.y * FIT_MARGIN) / boundingHeight : Number.POSITIVE_INFINITY;
let zoomLevel = Math.min(scaleX, scaleY);
if (!Number.isFinite(zoomLevel) || zoomLevel <= 0) zoomLevel = 1;
this.setScreenScale(zoomLevel);
// screen = (world - offset) * zoom 이므로, 도면 중심을 화면 중심에 맞추려면
// offset = worldCenter - (화면 절반 픽셀) / zoom
const worldCenterX = (boundingBox.minX + boundingBox.maxX) / 2;
const worldCenterY = (boundingBox.minY + boundingBox.maxY) / 2;
this.setScreenOffset(
new Point(
worldCenterX - canvasSize.x / 2 / zoomLevel,
worldCenterY - canvasSize.y / 2 / zoomLevel
)
);
const { zoom, offset } = fitView(entities, this.getCanvasSize());
this.setScreenScale(zoom);
this.setScreenOffset(offset);
this.viewFitted = true;
}
@@ -559,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
@@ -577,6 +552,8 @@ export class ScreenCanvasDrawController implements DrawController {
fontFamily: string;
bold: boolean;
italic: boolean;
/** 색을 그대로 칠함 — 자동색 뒤집기(paintColor) 건너뜀(테마 토큰 색) */
rawColor: boolean;
}> = {}
): void {
const opts = {
@@ -596,7 +573,7 @@ export class ScreenCanvasDrawController implements DrawController {
this.context.rotate(angle);
this.context.font = `${opts.italic ? 'italic ' : ''}${opts.bold ? 'bold ' : ''}${opts.fontSize}px ${opts.fontFamily}`;
this.context.textAlign = opts.textAlign;
this.context.fillStyle = paintColor(opts.textColor);
this.context.fillStyle = opts.rawColor ? opts.textColor : paintColor(opts.textColor);
this.context.textBaseline = 'middle';
this.context.fillText(label, 0, 0);
this.context.restore();
@@ -673,11 +650,19 @@ export class ScreenCanvasDrawController implements DrawController {
* @param width
* @param height
* @param color
* @param rawColor 색을 그대로 칠함 — 자동색 뒤집기(paintColor) 건너뜀(테마 토큰 색)
*/
public fillRectScreen(xMin: number, yMin: number, width: number, height: number, color: string) {
public fillRectScreen(
xMin: number,
yMin: number,
width: number,
height: number,
color: string,
rawColor = false
) {
if (this.batching) this.flushBatch();
// TODO see if we need to replace this with a call to fillPolygon
this.context.fillStyle = paintColor(color);
this.context.fillStyle = rawColor ? color : paintColor(color);
// 화면 좌표는 아래에서 위로 재는 규약이다(getCanvasPoint·drawLineScreen과 같다).
// yMin은 사각형의 아래 변이므로 캔버스 y로 옮길 때 높이를 함께 빼야 제자리다.
// 안 빼서 선택 그립이 선보다 8px 아래에 찍혔고, 줌을 해도 화면 픽셀 어긋남이라
@@ -2,13 +2,6 @@ import { Point } from '@flatten-js/core';
import { compact, round } from 'es-toolkit';
import { Actor } from 'xstate';
import {
CANVAS_INPUT_FIELD_BACKGROUND_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,
PINCH_ZOOM_EXPONENT,
SNAP_POINT_DISTANCE,
WHEEL_LINE_PX,
@@ -66,6 +59,7 @@ import {
type TextInputEvent,
} from '../tools/tool.types.ts';
import { activeToolAcceptsText, ImeSink, isImeKey } from './ime-sink.ts';
import { drawInputField } from './input-field-draw.ts';
import {
ABSOLUTE_POINT_REGEXP,
ABSOLUTE_POLAR_REGEXP,
@@ -127,36 +121,7 @@ export class InputController {
}
public draw(drawController: ScreenCanvasDrawController) {
const screenMouseLocation = drawController.getScreenMouseLocation();
// draw input field
drawController.fillRectScreen(
screenMouseLocation.x + CANVAS_INPUT_FIELD_MOUSE_OFFSET,
// fillRectScreen이 아래 변 기준으로 바로잡혔으므로, 칸이 있던 자리를 지키려고
// 높이만큼 내려 잡는다.
screenMouseLocation.y - CANVAS_INPUT_FIELD_MOUSE_OFFSET - CANVAS_INPUT_FIELD_HEIGHT,
CANVAS_INPUT_FIELD_WIDTH,
CANVAS_INPUT_FIELD_HEIGHT,
CANVAS_INPUT_FIELD_BACKGROUND_COLOR
);
this.ime.sync(activeToolAcceptsText());
// Draw text in input field (조합 중인 한글도 함께)
if (this.text || this.ime.composing) {
drawController.drawTextScreen(
this.text + this.ime.composing,
new Point(
screenMouseLocation.x + CANVAS_INPUT_FIELD_MOUSE_OFFSET + 2,
screenMouseLocation.y - CANVAS_INPUT_FIELD_MOUSE_OFFSET - CANVAS_INPUT_FIELD_HEIGHT - 2
),
{
textAlign: 'left',
textColor: CANVAS_INPUT_FIELD_TEXT_COLOR,
fontSize: CANVAS_INPUT_FIELD_FONT_SIZE,
}
);
}
const matchingCommands = this.getCommandSuggestions();
const toolInstruction = getLastStateInstructions();
const texts: string[] = [];
if (toolInstruction) {
@@ -166,11 +131,10 @@ export class InputController {
const roundedY = round(drawController.getWorldMouseLocation().y, 2);
texts.push(`${roundedX},${roundedY}`);
}
if (matchingCommands.length) {
// 입력 중인 문자로 시작하는 명령 후보. 예: C => CIRCLE (C), COPY (CO)
texts.push(...matchingCommands);
}
this.drawListBelowInputField(drawController, texts);
// 입력 중인 문자로 시작하는 명령 후보. 예: C => CIRCLE (C), COPY (CO)
texts.push(...this.getCommandSuggestions());
// 입력 글(조합 중인 한글도 함께)
drawInputField(drawController, this.text + this.ime.composing, texts);
}
public submitText(value: string) {
@@ -661,32 +625,6 @@ export class InputController {
type: ActorEvent.ESC,
});
}
private drawListBelowInputField(
drawController: ScreenCanvasDrawController,
texts: string[]
): void {
const screenMouseLocation = drawController.worldToTarget(
drawController.getWorldMouseLocation()
);
const startY =
screenMouseLocation.y - CANVAS_INPUT_FIELD_MOUSE_OFFSET - CANVAS_INPUT_FIELD_HEIGHT * 2 - 2;
const offsetY = CANVAS_INPUT_FIELD_HEIGHT;
texts.forEach((text, index) => {
drawController.drawTextScreen(
text,
new Point(
screenMouseLocation.x + CANVAS_INPUT_FIELD_MOUSE_OFFSET + 2,
startY - index * offsetY
),
{
textAlign: 'left',
textColor: CANVAS_INPUT_FIELD_INSTRUCTION_TEXT_COLOR,
fontSize: CANVAS_INPUT_FIELD_FONT_SIZE,
}
);
});
}
}
/**
@@ -0,0 +1,86 @@
/**
* 커서 따라다니는 입력창 — 캔버스에 직접 그림(`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,
}
);
});
}
@@ -37,4 +37,47 @@ assert.equal(paintColor('#f5f7fa'), '#000000', '밝은 바탕: 기본색은 검
assert.equal(paintColor('#ff0000'), '#ff0000', '밝은 바탕: 빨강 그대로');
assert.notEqual(paintColor('#ffff00'), '#ffff00', '밝은 바탕: 노랑 등 밝은 색은 대비 보정(기존)');
// 커서 입력창 — 칸 · 글 색이 두 테마 모두 달라야(자동색 둘을 쓰면 paintColor 가 같은 색으로 뒤집음)
const { inputFieldColors } = await import(
'../../../B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts'
);
const TOKENS: Record<string, Record<string, string>> = {
light: {
'--cad-chrome-raised': '#ffffff',
'--cad-line': '#e6e2e3',
'--cad-text': '#333333',
'--cad-text-dim': '#615e6e',
},
dark: {
'--cad-chrome-raised': '#251f38',
'--cad-line': '#322a45',
'--cad-text': '#e4e0f0',
'--cad-text-dim': '#b3aec6',
},
};
for (const name of ['light', 'dark']) {
theme = name;
const colors = inputFieldColors(
(token: string, fallback: string) => TOKENS[name][token] ?? fallback
);
assert.equal(
colors.background,
TOKENS[name]['--cad-chrome-raised'],
`${name}: 칸 = 토큰 색 그대로`
);
assert.notEqual(colors.background, colors.text, `${name}: 칸 · 글 색 다름`);
assert.notEqual(colors.background, colors.dimText, `${name}: 칸 · 안내 글 색 다름`);
assert.notEqual(colors.background, colors.border, `${name}: 칸 · 테두리 색 다름`);
// 옛 상수 짝(#000000 칸 · #FFF 글)은 paintColor 를 거치면 같은 색 — 입력창은 건너뛰어야
assert.equal(
paintColor('#000000'),
paintColor('#FFF'),
`${name}: 자동색 두 개는 같은 색으로 뒤집힘`
);
}
// 토큰 못 읽으면 폴백도 칸 · 글 다름
const fallback = inputFieldColors((_token: string, value: string) => value);
assert.notEqual(fallback.background, fallback.text, '폴백: 칸 · 글 색 다름');
assert.ok(!isAutoColor(fallback.background) && !isAutoColor(fallback.text), '폴백: 자동색 아님');
console.log('theme 자동색 시험 통과');