/** * 고른 것의 선 특성(색 · 선 두께 · 선종류) 읽기 · 바꾸기 한 벌 (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); notifyWindow(HtmlEvent.UPDATE_STATE); // 견본 색도 따라감 } /** * 색 선택기는 드래그하는 내내 값 변경을 쏜다. 그 하나하나를 되돌리기·변경통지에 실으면 * 색 한 번 고르는 동안 수십 건이 나간다(2026-09-01 지적). 첫 변경에 사본을 끼우고 끄는 동안에는 * 그 사본만 칠하고, 멎은 뒤 한 번만 되돌리기·통지에 싣는다(488 — Ctrl+Z 한 번에 옛 색). * 리본 특성 · 특성 판 색 칸이 같이 씀(506-3). */ const COLOR_COMMIT_DELAY_MS = 350; let colorCommitTimer: ReturnType | undefined; let painting: Entity[] | null = null; export function paintNowCommitLater(targets: Entity[], change: (entity: Entity) => void): void { if (painting) repaint(painting, change); else painting = restyle(targets, change, false); // 화면만 clearTimeout(colorCommitTimer); colorCommitTimer = setTimeout(() => { painting = null; setEntities([...getEntities()], true); // 멎었다 — 이번 한 번만 실어 보낸다 }, COLOR_COMMIT_DELAY_MS); }