사용자 편의성 점검 15건 중 반영 확정분 12건. 편집 손실 방지 - CAD에 미저장 표시(drawingDirty)를 두고, 도면을 바꾸기 전에 묻는다. 전에는 그은 선이 경고 없이 사라졌다(실측: 캔버스 서명이 원본과 동일). - 도면 전환을 겹쳐 눌러도 늦게 온 응답이 화면을 덮지 않게 가드를 뒀다. - 도면층 삭제·객체 이동을 되돌리기에 실어 Ctrl+Z로 돌아오게 했다. 확정한 도면은 읽기 전용 - runCommand 한 곳에서 보는 명령만 통과시킨다(허용 명시 방식). - 도면을 실을 때 앞 도면의 그리기 도구를 내린다 — 켜 둔 도구가 확정본에도 계속 그렸다. - 확정 버튼 자리를 [현재 도면 확정] / [수정]으로 가른다. 확정 해제는 [수정] 한 곳뿐 — 되돌리기·색 고르기로 확정이 풀리던 경로를 없앴다. 그 밖 - 잠금 도면층(도각·등고선·계류)은 마우스가 스쳐도 하이라이트하지 않는다. - 준비 중 도면 7종도 빈 도각으로 열린다(눌리지 않는 회색 버튼 제거). - 도면층 행이 잘리지 않게 패널을 300px로 넓히고 이름을 줄여 담는다. - 기본 그리기 색을 검정으로 — 흰 종이에 흰 선이라 안 보였다. - 색 고르는 동안 변경 통지가 연발하지 않는다. - 수량표는 앞 단계 산출물이라 도면에서 고치지 못하게 막고 안내한다. - 자동백업 칸을 도면별로 나눈다 — 되살리면 다른 도면에 붙었다. - 횡단 장 id를 시작 측점 기준으로 바꿔 구간이 달라져도 옛 확정이 안 붙는다. - 회사 도각 저장 전에 좌표를 검사하고, 기본 도각으로 되돌리기를 연다. - 횡단 파일이 없어도 나머지 도면 목록은 남는다. - 불러오는 중·실패를 화면에 표시한다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
231 lines
7.0 KiB
TypeScript
231 lines
7.0 KiB
TypeScript
/** 리본 안에 들어가는 위젯 패널 — 특성(색·굵기·선종류), 도면층, 문자 스타일 */
|
|
import type { FC } from 'react';
|
|
import { runCommandInput } from '../commands/run-command';
|
|
import type { Entity } from '../entities/Entity';
|
|
import { EntityName } from '../entities/Entity';
|
|
import type { TextEntity } from '../entities/TextEntity';
|
|
import {
|
|
getActiveLayerId,
|
|
getActiveLineColor,
|
|
getActiveLineDash,
|
|
getActiveLineWidth,
|
|
getActiveTextStyle,
|
|
getEntities,
|
|
getLayers,
|
|
getSelectedEntities,
|
|
setActiveLayerId,
|
|
setActiveLineColor,
|
|
setActiveLineDash,
|
|
setActiveLineWidth,
|
|
setActiveTextStyle,
|
|
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 지적). 끄는 동안에는 화면만 칠하고,
|
|
* 멎은 뒤 한 번만 되돌리기·통지에 싣는다.
|
|
*/
|
|
const COLOR_COMMIT_DELAY_MS = 350;
|
|
let colorCommitTimer: ReturnType<typeof setTimeout> | undefined;
|
|
function paintNowCommitLater(mutate: (entity: Entity) => void): void {
|
|
applyToSelection(mutate, false); // 화면만 — 되돌리기·미저장 표시는 아직
|
|
clearTimeout(colorCommitTimer);
|
|
colorCommitTimer = setTimeout(() => {
|
|
applyToSelection(() => {}, true); // 멎었다 — 이번 한 번만 실어 보낸다
|
|
}, COLOR_COMMIT_DELAY_MS);
|
|
}
|
|
|
|
export const PropertiesWidget: FC = () => {
|
|
const lineColor = getActiveLineColor();
|
|
const lineWidth = getActiveLineWidth();
|
|
const lineType = dashToLineType(getActiveLineDash());
|
|
|
|
return (
|
|
<div className="cad-ribbon-props">
|
|
<label className="cad-prop" title="객체 색상 (COLOR)">
|
|
<span>색상</span>
|
|
<input
|
|
type="color"
|
|
value={lineColor}
|
|
onChange={(event) => {
|
|
const color = event.target.value;
|
|
setActiveLineColor(color);
|
|
paintNowCommitLater((entity) => {
|
|
entity.lineColor = color;
|
|
});
|
|
}}
|
|
/>
|
|
</label>
|
|
<label className="cad-prop" title="선가중치 (LWEIGHT)">
|
|
<span>선가중치</span>
|
|
<select
|
|
value={lineWidth}
|
|
onChange={(event) => {
|
|
const width = Number(event.target.value);
|
|
setActiveLineWidth(width);
|
|
applyToSelection((entity) => {
|
|
entity.lineWidth = width;
|
|
});
|
|
}}
|
|
>
|
|
{LINE_WIDTHS.map((width) => (
|
|
<option key={width} value={width}>
|
|
{width}px
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="cad-prop" title="선종류 (LINETYPE)">
|
|
<span>선종류</span>
|
|
<select
|
|
value={lineType}
|
|
onChange={(event) => {
|
|
const dash = LINE_TYPES.find((type) => type.value === event.target.value)?.dash;
|
|
setActiveLineDash(dash ? [...dash] : undefined);
|
|
applyToSelection((entity) => {
|
|
entity.lineDash = dash ? [...dash] : undefined;
|
|
});
|
|
}}
|
|
>
|
|
{LINE_TYPES.map((type) => (
|
|
<option key={type.value} value={type.value}>
|
|
{type.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const LayersWidget: FC = () => {
|
|
const layers = getLayers();
|
|
const activeLayerId = getActiveLayerId();
|
|
|
|
return (
|
|
<div className="cad-ribbon-props">
|
|
<label className="cad-prop cad-prop--wide" title="현재 도면층">
|
|
<span>현재 도면층</span>
|
|
<select value={activeLayerId} onChange={(event) => setActiveLayerId(event.target.value)}>
|
|
{/* 잠금 도면층(배경)은 고를 수 없으니 보여 주지 않는다. 현재 층이 잠금이면
|
|
(전부 잠금인 도면) 표시가 비지 않게 그 항목만 남긴다. */}
|
|
{layers
|
|
.filter((layer) => !layer.isLocked || layer.id === activeLayerId)
|
|
.map((layer) => (
|
|
<option key={layer.id} value={layer.id}>
|
|
{layer.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="cad-tool"
|
|
data-size="small"
|
|
title="도면층 특성 관리자 (LAYER)"
|
|
onClick={() => runCommandInput('LAYER')}
|
|
>
|
|
<span className="cad-tool__glyph">▤</span>
|
|
<span className="cad-tool__label">도면층</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const TextStyleWidget: FC = () => {
|
|
const textStyle = getActiveTextStyle();
|
|
|
|
const handle = (patch: Partial<typeof textStyle>, live = false) => {
|
|
setActiveTextStyle(patch);
|
|
const mutate = (entity: Entity) => {
|
|
if (entity.getType() === EntityName.Text) {
|
|
(entity as TextEntity).setTextOptions(patch);
|
|
}
|
|
};
|
|
// 색만 드래그 중 연발한다 — 나머지 항목은 한 번 고르면 끝이라 바로 싣는다.
|
|
if (live) paintNowCommitLater(mutate);
|
|
else applyToSelection(mutate);
|
|
};
|
|
|
|
return (
|
|
<div className="cad-ribbon-props">
|
|
<label className="cad-prop cad-prop--wide" title="문자 글꼴 (STYLE)">
|
|
<span>글꼴</span>
|
|
<select
|
|
value={textStyle.fontFamily}
|
|
onChange={(event) => handle({ fontFamily: event.target.value })}
|
|
>
|
|
{FONT_FAMILIES.map((family) => (
|
|
<option key={family} value={family}>
|
|
{family}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="cad-prop" title="문자 높이">
|
|
<span>크기</span>
|
|
<input
|
|
type="number"
|
|
min={4}
|
|
max={120}
|
|
value={textStyle.fontSize}
|
|
onChange={(event) => {
|
|
const size = Number(event.target.value);
|
|
if (Number.isFinite(size) && size > 0) handle({ fontSize: size });
|
|
}}
|
|
/>
|
|
</label>
|
|
<label className="cad-prop" title="굵게">
|
|
<span>굵게</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={textStyle.bold}
|
|
onChange={(event) => handle({ bold: event.target.checked })}
|
|
/>
|
|
</label>
|
|
<label className="cad-prop" title="기울임">
|
|
<span>기울임</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={textStyle.italic}
|
|
onChange={(event) => handle({ italic: event.target.checked })}
|
|
/>
|
|
</label>
|
|
<label className="cad-prop" title="문자 색상">
|
|
<span>색상</span>
|
|
<input
|
|
type="color"
|
|
value={textStyle.textColor}
|
|
onChange={(event) => handle({ textColor: event.target.value }, true)}
|
|
/>
|
|
</label>
|
|
</div>
|
|
);
|
|
};
|