This commit is contained in:
2026-07-19 19:18:14 +09:00
parent ef0dab9bc3
commit 6ec8fca60e
23 changed files with 1130 additions and 790 deletions
@@ -4,10 +4,18 @@ import { Actor } from 'xstate';
import { HtmlEvent, type Layer } from '../App.types';
import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json';
import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage';
import type { Entity } from '../entities/Entity';
import { EntityName } from '../entities/Entity';
import { TextEntity } from '../entities/TextEntity';
import {
getActiveLayerId,
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
getActiveTextStyle,
getActiveToolActor,
getAngleStep,
getEntities,
getGridEnabled,
getInputController,
getLastStateInstructions,
@@ -17,8 +25,13 @@ import {
getSnapEnabled,
redo,
setActiveLayerId,
setActiveLineColor,
setActiveLineDash,
setActiveLineWidth,
setActiveTextStyle,
setActiveToolActor,
setAngleStep,
setEntities,
setGridEnabled,
setLayers,
setSnapEnabled,
@@ -69,6 +82,20 @@ const RIBBON_GROUPS: { label: string; tools: RibbonTool[] }[] = [
const COMMANDS = Object.values(Tool);
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] },
];
const LINE_WIDTHS = [1, 2, 3, 4, 5];
const FONT_FAMILIES = ['Noto Sans KR', 'Malgun Gothic', 'Pretendard', 'Arial', 'monospace'];
const dashToLineType = (dash: number[] | undefined): string =>
LINE_TYPES.find((type) => JSON.stringify(type.dash) === JSON.stringify(dash))?.value ?? 'solid';
export const Toolbar: FC = () => {
const [activeTool, setActiveTool] = useState<Tool>(Tool.LINE);
const [zoom, setZoom] = useState(1);
@@ -84,6 +111,10 @@ export const Toolbar: FC = () => {
const [ortho, setOrtho] = useState(getAngleStep() === 90);
const [command, setCommand] = useState('');
const [commandLog, setCommandLog] = useState('준비');
const [lineColor, setLineColorLocal] = useState(getActiveLineColor());
const [lineWidth, setLineWidthLocal] = useState(getActiveLineWidth());
const [lineType, setLineTypeLocal] = useState(dashToLineType(getActiveLineDash()));
const [textStyle, setTextStyleLocal] = useState(getActiveTextStyle());
const refresh = useCallback(() => {
setActiveTool(getActiveToolActor()?.getSnapshot()?.context.type ?? Tool.LINE);
@@ -99,6 +130,10 @@ export const Toolbar: FC = () => {
setSnap(getSnapEnabled());
setGrid(getGridEnabled());
setOrtho(getAngleStep() === 90);
setLineColorLocal(getActiveLineColor());
setLineWidthLocal(getActiveLineWidth());
setLineTypeLocal(dashToLineType(getActiveLineDash()));
setTextStyleLocal({ ...getActiveTextStyle() });
}, []);
useEffect(() => {
@@ -136,6 +171,55 @@ export const Toolbar: FC = () => {
setZoom(controller.getScreenScale());
};
/** 선택 객체가 있으면 스타일을 즉시 적용하고, 없으면 이후 그리기 기본값만 바꾼다. */
const applyToSelection = useCallback((mutate: (entity: Entity) => void): boolean => {
const selected = getSelectedEntities();
if (!selected.length) return false;
for (const entity of selected) {
mutate(entity);
}
setEntities([...getEntities()], true);
return true;
}, []);
const handleLineColor = (color: string) => {
setActiveLineColor(color);
setLineColorLocal(color);
if (applyToSelection((entity) => (entity.lineColor = color))) {
setCommandLog('선택 객체 색상 변경');
}
};
const handleLineWidth = (width: number) => {
setActiveLineWidth(width);
setLineWidthLocal(width);
if (applyToSelection((entity) => (entity.lineWidth = width))) {
setCommandLog('선택 객체 선굵기 변경');
}
};
const handleLineType = (value: string) => {
const dash = LINE_TYPES.find((type) => type.value === value)?.dash;
setActiveLineDash(dash ? [...dash] : undefined);
setLineTypeLocal(value);
if (applyToSelection((entity) => (entity.lineDash = dash ? [...dash] : undefined))) {
setCommandLog('선택 객체 선종류 변경');
}
};
const handleTextStyle = (patch: Partial<typeof textStyle>) => {
setActiveTextStyle(patch);
setTextStyleLocal((previous) => ({ ...previous, ...patch }));
const applied = applyToSelection((entity) => {
if (entity.getType() === EntityName.Text) {
(entity as TextEntity).setTextOptions(patch);
}
});
if (applied) {
setCommandLog('선택 문자 스타일 변경');
}
};
return (
<>
<header className="cad-titlebar controls">
@@ -195,6 +279,83 @@ export const Toolbar: FC = () => {
<span className="cad-ribbon-group__label">{group.label}</span>
</section>
))}
<section className="cad-ribbon-group">
<div className="cad-ribbon-tools cad-ribbon-props">
<label className="cad-prop" title="선 색상">
<span></span>
<input
type="color"
value={lineColor}
onChange={(event) => handleLineColor(event.target.value)}
/>
</label>
<label className="cad-prop" title="선 굵기">
<span></span>
<select
value={lineWidth}
onChange={(event) => handleLineWidth(Number(event.target.value))}
>
{LINE_WIDTHS.map((width) => (
<option key={width} value={width}>
{width}px
</option>
))}
</select>
</label>
<label className="cad-prop" title="선 종류">
<span></span>
<select value={lineType} onChange={(event) => handleLineType(event.target.value)}>
{LINE_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</label>
</div>
<span className="cad-ribbon-group__label"></span>
</section>
<section className="cad-ribbon-group">
<div className="cad-ribbon-tools cad-ribbon-props">
<label className="cad-prop" title="폰트">
<span></span>
<select
value={textStyle.fontFamily}
onChange={(event) => handleTextStyle({ 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) {
handleTextStyle({ fontSize: size });
}
}}
/>
</label>
<label className="cad-prop" title="문자 색상">
<span></span>
<input
type="color"
value={textStyle.textColor}
onChange={(event) => handleTextStyle({ textColor: event.target.value })}
/>
</label>
</div>
<span className="cad-ribbon-group__label"></span>
</section>
</nav>
<aside className="cad-inspector controls" data-collapsed={panelCollapsed}>