feat(488-2 CAD 선 속성): 색 · 선 두께 · 선종류 = 고른 것의 값(다르면 빈 칸 · 색 = 점선 빈 견본) · 고른 것만 바꿈 · 폴리선 자식까지 · 사본 갈아 끼움으로 Ctrl+Z 한 번 = 옛 값 — 새 helpers/selection-style.ts 를 리본 · 특성 판 · COLOR · LINETYPE · LWEIGHT · TRANSPARENCY · MATCHPROP · SETBYLAYER · LAYMCH · LAYCUR 가 같이 씀 · 시험 selection-style.test.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:13:08 +09:00
co-authored by Claude Opus 5.5
parent add2887c30
commit 99df7c2512
8 changed files with 322 additions and 97 deletions
+2
View File
@@ -334,6 +334,8 @@ body > canvas[data-id="canvas"] {
border-radius: 3px;
cursor: pointer;
}
/* 고른 것들 색이 다름(488) — 빈 견본(투명 · 점선 테두리) */
input[type="color"][data-mixed="true"]::-webkit-color-swatch { background: transparent !important; border: 1px dashed var(--cad-text-muted); }
/* 위젯 패널 안의 작은 명령 버튼은 한 줄 높이에 맞춘다 */
.cad-ribbon-props .cad-tool[data-size="small"] {
height: 22px;
@@ -2,25 +2,31 @@
import type { FC } from 'react';
import type { Entity } from '../entities/Entity';
import { polylineLength, sampleEntityPoints } from '../helpers/geometry/sample-entity';
import { getEntities, getLayers, getSelectedEntities, setEntities } from '../state';
import { dashToLineType, LINE_TYPES, LINE_WIDTHS } from './RibbonWidgets';
import {
commonValue,
LINE_TYPES,
LINE_WIDTHS,
restyle,
selectionLineStyle,
styleTargets,
} from '../helpers/selection-style';
import { getLayers, getSelectedEntities } from '../state';
interface PropertiesEditorProps {
compact?: boolean;
}
function applyToSelection(mutate: (entity: Entity) => void): void {
const selected = getSelectedEntities();
if (!selected.length) return;
for (const entity of selected) mutate(entity);
setEntities([...getEntities()], true);
}
/** 고른 것을 사본으로 바꿈 — 되돌리기 한 칸(488) · 선 특성은 글 뺌 */
const applyToSelection = (mutate: (entity: Entity) => void) => restyle(getSelectedEntities(), mutate);
const applyToLines = (mutate: (entity: Entity) => void) =>
restyle(styleTargets(getSelectedEntities()), mutate);
/** 여러 객체가 값이 다르면 '*가지각색' 대신 첫 객체 값을 보여 준다 (AutoCAD와 같은 관행) */
/** 여러 객체가 값이 다르면 빈 칸(488 · 리본과 같은 규칙 · 고르면 모두에 적용) */
export const PropertiesEditor: FC<PropertiesEditorProps> = ({ compact = false }) => {
const selected = getSelectedEntities();
const layers = getLayers();
const first = selected[0];
const view = selectionLineStyle();
if (!first) {
return <p className="cad-properties__empty">선택된 객체가 없습니다.</p>;
@@ -39,9 +45,10 @@ export const PropertiesEditor: FC<PropertiesEditorProps> = ({ compact = false })
<span>색상</span>
<input
type="color"
value={first.lineColor}
value={view.color || '#000000'}
data-mixed={view.color ? undefined : 'true'}
onChange={(event) =>
applyToSelection((entity) => {
applyToLines((entity) => {
entity.lineColor = event.target.value;
})
}
@@ -51,13 +58,14 @@ export const PropertiesEditor: FC<PropertiesEditorProps> = ({ compact = false })
<label>
<span>선 두께</span>
<select
value={first.lineWidth}
value={view.width}
onChange={(event) =>
applyToSelection((entity) => {
applyToLines((entity) => {
entity.lineWidth = Number(event.target.value);
})
}
>
<option value="" hidden />
{LINE_WIDTHS.map((width) => (
<option key={width} value={width}>
{width}px
@@ -69,14 +77,15 @@ export const PropertiesEditor: FC<PropertiesEditorProps> = ({ compact = false })
<label>
<span>선종류</span>
<select
value={dashToLineType(first.lineDash)}
value={view.lineType}
onChange={(event) => {
const dash = LINE_TYPES.find((type) => type.value === event.target.value)?.dash;
applyToSelection((entity) => {
applyToLines((entity) => {
entity.lineDash = dash ? [...dash] : undefined;
});
}}
>
<option value="" hidden />
{LINE_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
@@ -88,13 +97,14 @@ export const PropertiesEditor: FC<PropertiesEditorProps> = ({ compact = false })
<label>
<span>레이어</span>
<select
value={first.layerId}
value={commonValue(selected, (entity) => entity.layerId)}
onChange={(event) =>
applyToSelection((entity) => {
entity.layerId = event.target.value;
})
}
>
<option value="" hidden />
{layers.map((layer) => (
<option key={layer.id} value={layer.id}>
{layer.name}
@@ -4,11 +4,16 @@ import { runCommandInput } from '../commands/run-command';
import type { Entity } from '../entities/Entity';
import { EntityName } from '../entities/Entity';
import type { TextEntity } from '../entities/TextEntity';
import {
LINE_TYPES,
LINE_WIDTHS,
repaint,
restyle,
selectionLineStyle,
styleTargets,
} from '../helpers/selection-style';
import {
getActiveLayerId,
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
getActiveTextStyle,
getEntities,
getLayers,
@@ -21,50 +26,31 @@ import {
setEntities,
} from '../state';
export const LINE_TYPES: { value: string; label: string; dash: number[] | undefined }[] = [
{ value: 'solid', label: '실선', dash: undefined },
{ value: 'dashed', label: '파선', dash: [10, 5] },
{ value: 'dashdot', label: '1점쇄선', dash: [12, 4, 2, 4] },
{ value: 'dotted', label: '점선', dash: [2, 4] },
];
export const LINE_WIDTHS = [1, 2, 3, 4, 5];
const FONT_FAMILIES = ['Noto Sans KR', 'Malgun Gothic', 'Pretendard', 'Arial', 'monospace'];
export const dashToLineType = (dash: number[] | undefined): string =>
LINE_TYPES.find((type) => JSON.stringify(type.dash) === JSON.stringify(dash))?.value ?? 'solid';
/** 선택 객체가 있으면 즉시 적용하고, 없으면 이후 그리기 기본값만 바꾼다. */
function applyToSelection(mutate: (entity: Entity) => void, trackInUndoStack = true): boolean {
const selected = getSelectedEntities();
if (!selected.length) return false;
for (const entity of selected) {
mutate(entity);
}
setEntities([...getEntities()], trackInUndoStack);
return true;
}
/**
* 색 선택기는 드래그하는 내내 값 변경을 쏜다. 그 하나하나를 되돌리기·변경통지에 실으면
* 색 한 번 고르는 동안 수십 건이 나간다(2026-09-01 지적). 끄는 동안에는 화면만 칠하고,
* 멎은 뒤 한 번만 되돌리기·통지에 싣는다.
* 색 한 번 고르는 동안 수십 건이 나간다(2026-09-01 지적). 첫 변경에 사본을 끼우고 끄는 동안에는
* 그 사본만 칠하고, 멎은 뒤 한 번만 되돌리기·통지에 싣는다(488 — Ctrl+Z 한 번에 옛 색).
*/
const COLOR_COMMIT_DELAY_MS = 350;
let colorCommitTimer: ReturnType<typeof setTimeout> | undefined;
function paintNowCommitLater(mutate: (entity: Entity) => void): void {
applyToSelection(mutate, false); // 화면만 — 되돌리기·미저장 표시는 아직
let painting: Entity[] | null = null;
function paintNowCommitLater(targets: Entity[], change: (entity: Entity) => void): void {
if (painting) repaint(painting, change);
else painting = restyle(targets, change, false); // 화면만
clearTimeout(colorCommitTimer);
colorCommitTimer = setTimeout(() => {
applyToSelection(() => {}, true); // 멎었다 — 이번 한 번만 실어 보낸다
painting = null;
setEntities([...getEntities()], true); // 멎었다 — 이번 한 번만 실어 보낸다
}, COLOR_COMMIT_DELAY_MS);
}
/** 고른 선 있으면 그것만 바꿈(488 — 그리기 기본값 그대로) · 없으면 그리기 기본값만 */
const selectedLines = () => styleTargets(getSelectedEntities());
export const PropertiesWidget: FC = () => {
const lineColor = getActiveLineColor();
const lineWidth = getActiveLineWidth();
const lineType = dashToLineType(getActiveLineDash());
const view = selectionLineStyle();
return (
<div className="cad-ribbon-props">
@@ -72,11 +58,13 @@ export const PropertiesWidget: FC = () => {
<span>색상</span>
<input
type="color"
value={lineColor}
value={view.color || '#000000'}
data-mixed={view.color ? undefined : 'true'}
onChange={(event) => {
const color = event.target.value;
setActiveLineColor(color);
paintNowCommitLater((entity) => {
const lines = selectedLines();
if (!lines.length) return setActiveLineColor(color);
paintNowCommitLater(lines, (entity) => {
entity.lineColor = color;
});
}}
@@ -85,15 +73,17 @@ export const PropertiesWidget: FC = () => {
<label className="cad-prop" title="선 두께 (LWEIGHT)">
<span>선 두께</span>
<select
value={lineWidth}
value={view.width}
onChange={(event) => {
const width = Number(event.target.value);
setActiveLineWidth(width);
applyToSelection((entity) => {
const lines = selectedLines();
if (!lines.length) return setActiveLineWidth(width);
restyle(lines, (entity) => {
entity.lineWidth = width;
});
}}
>
<option value="" hidden />
{LINE_WIDTHS.map((width) => (
<option key={width} value={width}>
{width}px
@@ -104,15 +94,17 @@ export const PropertiesWidget: FC = () => {
<label className="cad-prop" title="선종류 (LINETYPE)">
<span>선종류</span>
<select
value={lineType}
value={view.lineType}
onChange={(event) => {
const dash = LINE_TYPES.find((type) => type.value === event.target.value)?.dash;
setActiveLineDash(dash ? [...dash] : undefined);
applyToSelection((entity) => {
const lines = selectedLines();
if (!lines.length) return setActiveLineDash(dash ? [...dash] : undefined);
restyle(lines, (entity) => {
entity.lineDash = dash ? [...dash] : undefined;
});
}}
>
<option value="" hidden />
{LINE_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
@@ -163,14 +155,11 @@ export const TextStyleWidget: FC = () => {
const handle = (patch: Partial<typeof textStyle>, live = false) => {
setActiveTextStyle(patch);
const mutate = (entity: Entity) => {
if (entity.getType() === EntityName.Text) {
(entity as TextEntity).setTextOptions(patch);
}
};
const texts = getSelectedEntities().filter((entity) => entity.getType() === EntityName.Text);
const mutate = (entity: Entity) => (entity as TextEntity).setTextOptions(patch);
// 색만 드래그 중 연발한다 — 나머지 항목은 한 번 고르면 끝이라 바로 싣는다.
if (live) paintNowCommitLater(mutate);
else applyToSelection(mutate);
if (live) paintNowCommitLater(texts, mutate);
else restyle(texts, mutate);
};
return (
@@ -0,0 +1,107 @@
/**
* 고른 것의 선 특성(색 · 선 두께 · 선종류) 읽기 · 바꾸기 한 벌 (488 · 리본 특성 · 특성 판 · 특성 명령).
*
* 읽기 = 고른 것의 선 잎(폴리선 → 자식 선 · 글 뺌)이 모두 같으면 그 값 · 다르거나 목록 밖이면 빈 값 ·
* 고른 선이 없으면 그리기 기본값.
* 바꾸기 = id 그대로 사본으로 갈아 끼움(폴리선 자식까지) — 되돌리기 한 칸 · Ctrl+Z 한 번에 옛 값.
*/
import { type Entity, EntityName } from '../entities/Entity';
import type { PolyLineEntity } from '../entities/PolyLineEntity';
import {
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
getEntities,
getSelectedEntities,
notifyWindow,
setEntities,
} from '../state';
import { HtmlEvent } from '../App.types';
import { cloneKeepingLook } from '../tools/move-tool.helpers';
export const LINE_TYPES: { value: string; label: string; dash: number[] | undefined }[] = [
{ value: 'solid', label: '실선', dash: undefined },
{ value: 'dashed', label: '파선', dash: [10, 5] },
{ value: 'dashdot', label: '1점쇄선', dash: [12, 4, 2, 4] },
{ value: 'dotted', label: '점선', dash: [2, 4] },
];
export const LINE_WIDTHS = [1, 2, 3, 4, 5];
/** 점선 → 목록 값 · `[]` · 없음 = 실선 · 목록 밖 = 빈 값 */
export const dashToLineType = (dash: number[] | undefined): string =>
dash?.length
? (LINE_TYPES.find((type) => JSON.stringify(type.dash) === JSON.stringify(dash))?.value ?? '')
: 'solid';
/** 선 메뉴가 보는 것 — 글 뺌(글 색은 문자 스타일 칸) */
export const styleTargets = (entities: Entity[]): Entity[] =>
entities.filter((entity) => entity.getType() !== EntityName.Text);
/** 그림에 쓰는 잎 — 폴리선은 자식 선이 제 색 · 두께로 그림 */
const leaves = (entity: Entity): Entity[] =>
entity.getType() === EntityName.PolyLine ? (entity as PolyLineEntity).getEntities() : [entity];
/** 모두 같으면 그 값 · 다르거나 없으면 `''`(빈 칸) */
export function commonValue(entities: Entity[], read: (entity: Entity) => string): string {
const values = new Set(entities.flatMap(leaves).map(read));
return values.size === 1 ? [...values][0] : '';
}
export interface LineStyleView {
color: string;
width: string;
lineType: string;
}
/** 리본 · 특성 판이 보일 값 — 고른 선 있으면 그 값(다르면 `''`) · 없으면 그리기 기본값 */
export function selectionLineStyle(): LineStyleView {
const targets = styleTargets(getSelectedEntities());
if (!targets.length) {
return {
color: getActiveLineColor(),
width: String(getActiveLineWidth()),
lineType: dashToLineType(getActiveLineDash()),
};
}
const width = commonValue(targets, (entity) => String(entity.lineWidth));
return {
color: commonValue(targets, (entity) => entity.lineColor.toLowerCase()),
width: LINE_WIDTHS.map(String).includes(width) ? width : '',
lineType: commonValue(targets, (entity) => dashToLineType(entity.lineDash)),
};
}
/** 사본에 바꿈 — 폴리선은 자식까지 */
function applyDeep(entity: Entity, change: (entity: Entity) => void): void {
change(entity);
if (entity.getType() === EntityName.PolyLine) {
for (const child of (entity as PolyLineEntity).getEntities()) change(child);
}
}
/**
* 바꾼 사본(id 그대로)을 제자리에 끼움 · `track` = 되돌리기 한 칸.
* 돌려준 사본은 이어서 칠할 수 있음(`repaint` · 색 끌기).
*/
export function restyle(targets: Entity[], change: (entity: Entity) => void, track = true): Entity[] {
if (!targets.length) return [];
const copies = targets.map((entity) => {
const copy = cloneKeepingLook(entity, true);
applyDeep(copy, change);
return copy;
});
const byId = new Map(copies.map((copy) => [copy.id, copy]));
setEntities(
getEntities().map((entity) => byId.get(entity.id) ?? entity),
track
);
notifyWindow(HtmlEvent.UPDATE_STATE);
return copies;
}
/** 이미 끼운 사본을 다시 칠함(화면만 · 되돌리기 없음) */
export function repaint(copies: Entity[], change: (entity: Entity) => void): void {
for (const copy of copies) applyDeep(copy, change);
setEntities([...getEntities()], false);
}
@@ -15,6 +15,7 @@ import {
import { Tool } from '../../tools';
import { createSequenceTool } from '../factories/sequence-tool';
import { findDuplicateEntities, reverseEntity } from './modify.helpers';
import { restyle } from '../../helpers/selection-style';
export const matchPropToolStateMachine = createSequenceTool({
tool: Tool.MATCHPROP,
@@ -25,12 +26,12 @@ export const matchPropToolStateMachine = createSequenceTool({
],
commit: (input) => {
const source = input.entity(0);
const target = input.entity(1);
target.lineColor = source.lineColor;
target.lineWidth = source.lineWidth;
target.lineDash = source.lineDash;
target.layerId = source.layerId;
setEntities([...getEntities()], true);
restyle([input.entity(1)], (target) => {
target.lineColor = source.lineColor;
target.lineWidth = source.lineWidth;
target.lineDash = source.lineDash ? [...source.lineDash] : undefined;
target.layerId = source.layerId;
});
},
});
@@ -67,16 +68,15 @@ export const setByLayerToolStateMachine = createSequenceTool({
helpers: false,
steps: [{ kind: 'selection', instructions: 'ByLayer로 되돌릴 객체를 선택한 뒤 ENTER.' }],
commit: (input) => {
let applied = 0;
for (const entity of input.entities(0)) {
const targets = input.entities(0).filter((entity) => getLayerById(entity.layerId));
restyle(targets, (entity) => {
const layer = getLayerById(entity.layerId);
if (!layer) continue;
if (!layer) return;
if (layer.color) entity.lineColor = layer.color;
if (layer.lineWidth) entity.lineWidth = layer.lineWidth;
entity.lineDash = layer.lineDash ? [...layer.lineDash] : undefined;
applied += 1;
}
setEntities([...getEntities()], true);
});
const applied = targets.length;
setSelectedEntityIds([]);
toast.success(`${applied}개 객체를 레이어 특성으로 되돌렸습니다.`);
},
@@ -1,5 +1,6 @@
/** 도면층 명령 (조사표 3절 도면층 패널) */
import { toast } from 'react-toastify';
import { restyle } from '../../helpers/selection-style';
import type { Layer } from '../../App.types';
import { openInspector } from '../../components/ui-state';
import type { Entity } from '../../entities/Entity';
@@ -186,10 +187,9 @@ export const layerMatchToolStateMachine = createSequenceTool({
],
commit: (input) => {
const layerId = input.entity(1).layerId;
for (const entity of input.entities(0)) {
restyle(input.entities(0), (entity) => {
entity.layerId = layerId;
}
setEntities([...getEntities()], true);
});
setSelectedEntityIds([]);
toast.success(`${input.entities(0).length}개 객체를 ${layerNameOf(layerId)}(으)로 옮겼습니다.`);
},
@@ -201,10 +201,9 @@ export const layerToCurrentToolStateMachine = createSequenceTool({
steps: [{ kind: 'selection', instructions: '현재 레이어로 옮길 객체를 선택한 뒤 ENTER.' }],
commit: (input) => {
const layerId = getActiveLayerId();
for (const entity of input.entities(0)) {
restyle(input.entities(0), (entity) => {
entity.layerId = layerId;
}
setEntities([...getEntities()], true);
});
setSelectedEntityIds([]);
toast.success(`${input.entities(0).length}개 객체를 현재 레이어로 옮겼습니다.`);
},
@@ -5,7 +5,8 @@ import {
setQuickPropertiesVisible,
isQuickPropertiesVisible,
} from '../../components/ui-state';
import { getEntities, setEntities, setSelectedEntityIds } from '../../state';
import { restyle } from '../../helpers/selection-style';
import { setSelectedEntityIds } from '../../state';
import { Tool } from '../../tools';
import { createSequenceTool } from '../factories/sequence-tool';
@@ -60,10 +61,9 @@ export const colorToolStateMachine = createSequenceTool({
toast.warn('#rrggbb 형식이나 색 이름을 입력하십시오.');
return;
}
for (const entity of input.entities(0)) {
restyle(input.entities(0), (entity) => {
entity.lineColor = color;
}
setEntities([...getEntities()], true);
});
setSelectedEntityIds([]);
toast.success(`색상 ${color}`);
},
@@ -87,10 +87,9 @@ export const lineTypeToolStateMachine = createSequenceTool({
return;
}
const dash = LINE_DASHES[key] ?? LINE_DASHES[key.toUpperCase()];
for (const entity of input.entities(0)) {
restyle(input.entities(0), (entity) => {
entity.lineDash = dash ? [...dash] : undefined;
}
setEntities([...getEntities()], true);
});
setSelectedEntityIds([]);
toast.success(`선종류 ${key}`);
},
@@ -105,10 +104,9 @@ export const lineWeightToolStateMachine = createSequenceTool({
],
commit: (input) => {
const width = Math.max(1, Math.round(input.number(1)));
for (const entity of input.entities(0)) {
restyle(input.entities(0), (entity) => {
entity.lineWidth = width;
}
setEntities([...getEntities()], true);
});
setSelectedEntityIds([]);
toast.success(`선 두께 ${width}px`);
},
@@ -123,10 +121,9 @@ export const transparencyToolStateMachine = createSequenceTool({
],
commit: (input) => {
const percent = Math.min(90, Math.max(0, input.number(1)));
for (const entity of input.entities(0)) {
restyle(input.entities(0), (entity) => {
entity.opacity = 1 - percent / 100;
}
setEntities([...getEntities()], true);
});
setSelectedEntityIds([]);
toast.success(`투명도 ${percent}%를 적용했습니다.`);
},
@@ -0,0 +1,121 @@
/**
* 488-2 — 선 특성 메뉴 = 고른 것의 값(다르면 빈 칸) · 바꾸면 고른 것만 · 폴리선 자식까지 · Ctrl+Z 한 번에 옛 값.
* 짝: `src/helpers/selection-style.ts`(리본 특성 · 특성 판 · COLOR · LWEIGHT 등 명령 같이 씀).
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad selection-style
*/
// 앱과 같은 flatten 사본(시험 쪽 사본 점은 앱 Segment 가 못 받음)
import { Point } from "../../../B07_DesignDetail/openwebcad/node_modules/@flatten-js/core";
import { beforeEach, describe, expect, it } from "vitest";
import type { Entity } from "../../../B07_DesignDetail/openwebcad/src/entities/Entity";
import { LineEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/LineEntity";
import { PolyLineEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/PolyLineEntity";
import { TextEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/TextEntity";
import {
dashToLineType,
restyle,
selectionLineStyle,
styleTargets,
} from "../../../B07_DesignDetail/openwebcad/src/helpers/selection-style";
import {
getActiveLineWidth,
getEntities,
getSelectedEntities,
resetUndoBaseline,
setActiveLineWidth,
setEntities,
setLayers,
setSelectedEntityIds,
undo,
} from "../../../B07_DesignDetail/openwebcad/src/state";
const line = (color: string, width = 1, dash?: number[]) => {
const entity = new LineEntity("L", new Point(0, 0), new Point(10, 0));
entity.lineColor = color;
entity.lineWidth = width;
entity.lineDash = dash;
return entity;
};
/** 엔진 꼴 — 부모 값과 자식 선 값이 따로 */
const poly = (childColor: string) => {
const kids = [line(childColor), line(childColor)];
const entity = new PolyLineEntity("L", kids);
entity.lineColor = "#ffffff";
return entity;
};
const leafWidths = (entity: Entity) =>
entity instanceof PolyLineEntity ? entity.getEntities().map((kid) => kid.lineWidth) : [entity.lineWidth];
const byId = (id: string) => getEntities().find((entity) => entity.id === id) as Entity;
/** 실은 도면 = 선 a(빨강) · 폴리선 b(자식 파랑) · 글 · 방금 그린 선 d */
let a: LineEntity;
let b: PolyLineEntity;
let d: LineEntity;
beforeEach(() => {
setLayers([{ id: "L", name: "L", isVisible: true, isLocked: false }]);
a = line("#ff0000");
b = poly("#0000ff");
const text = new TextEntity("L", "글", new Point(0, 0));
setEntities([a, b, text], false);
resetUndoBaseline();
d = line("#00ff00");
setEntities([...getEntities(), d], true); // 방금 그린 선
setSelectedEntityIds([a.id, b.id, text.id]);
setActiveLineWidth(1);
});
describe("selectionLineStyle", () => {
it("다른 색 = 빈 칸 · 같은 두께 · 선종류 = 그 값 · 글은 안 봄", () => {
expect(styleTargets(getSelectedEntities())).toHaveLength(2);
expect(selectionLineStyle()).toEqual({ color: "", width: "1", lineType: "solid" });
});
it("폴리선은 자식 값을 봄 · 목록 밖 두께 · 점선 = 빈 칸", () => {
setSelectedEntityIds([b.id]);
expect(selectionLineStyle().color).toBe("#0000ff");
a.lineWidth = 0.5;
a.lineDash = [6, 4]; // 엔진 점선(목록 밖)
setSelectedEntityIds([a.id]);
expect(selectionLineStyle()).toEqual({ color: "#ff0000", width: "", lineType: "" });
expect([dashToLineType([]), dashToLineType(undefined), dashToLineType([10, 5])]).toEqual([
"solid",
"solid",
"dashed",
]);
});
it("고른 선 없으면 그리기 기본값", () => {
setSelectedEntityIds([]);
setActiveLineWidth(4);
expect(selectionLineStyle().width).toBe("4");
});
});
describe("restyle", () => {
it("고른 것만 · 폴리선 자식까지 · 고름 · id 그대로 · 기본값 그대로", () => {
restyle(styleTargets(getSelectedEntities()), (entity) => {
entity.lineWidth = 3;
});
expect(leafWidths(byId(a.id))).toEqual([3]);
expect(leafWidths(byId(b.id))).toEqual([3, 3]);
expect(byId(d.id).lineWidth).toBe(1);
expect(getSelectedEntities()).toHaveLength(3);
expect(selectionLineStyle().width).toBe("3");
expect(getActiveLineWidth()).toBe(1);
});
it("Ctrl+Z 한 번 = 옛 값 · 방금 그린 선 남음", () => {
restyle(styleTargets(getSelectedEntities()), (entity) => {
entity.lineWidth = 3;
});
undo();
expect(leafWidths(byId(a.id))).toEqual([1]);
expect(leafWidths(byId(b.id))).toEqual([1, 1]);
expect(getEntities().map((entity) => entity.id)).toContain(d.id);
});
it("고른 것 없으면 아무것도 안 바뀜 · 되돌리기 칸 안 쌓임", () => {
expect(restyle([], () => {})).toEqual([]);
undo();
expect(getEntities().map((entity) => entity.id)).not.toContain(d.id); // 한 칸 전 = 그리기 전
});
});