저장소 사고로 잃은 4개 커밋(38eb7cd4·b8a11756·281528bf·f60b488d)의 작업물을 하나로 다시 담았다. 내용은 동일하다. ■ 구조 - commands/: 명령 정의(id·AutoCAD 별칭·글리프·도구/즉시실행)를 단일 소스로 두고 리본·명령행·단축키가 모두 이 레지스트리를 읽는다. tools/tool.consts.ts 폐지. - ribbon/: 탭→패널→명령 데이터(ribbon.config.ts)와 범용 렌더러 분리. AutoCAD 배치(홈·삽입·주석·뷰·출력)와 패널 확장(▾)을 따른다. - tools/factories/sequence-tool.ts: 점·숫자·문자·객체·선택 단계를 선언하면 xstate 머신을 만들어 주는 공장. 명령당 170줄 보일러플레이트 제거. - helpers/geometry/: 3점 호·정다각형·타원·스플라인·구름형·평행이동·해치 스캔선· 점렬 샘플링 등 상태 없는 순수 함수. - Toolbar 483줄을 QuickAccessBar/Ribbon/InspectorPanel/StatusBar/CommandLine/ ViewControls/PropertiesEditor/QuickProperties로 분해. ■ 명령 (조사표 기준) - 1절 그리기 21건, 2절 수정 27건, 3절 도면층·특성·그룹·유틸리티 39건 전부 반영. - 5절 주석 34건 중 26건 반영(문자·치수 16종·지시선·표·구름형·주석 축척). - HatchEntity 추가(solid·pattern·cross·gradient) + JSON 왕복, Layer에 색·선가중치· 선종류·동결·투명도 필드 추가, 그리기 루프가 동결·숨김·투명도를 반영. ■ 화면 실측에서 고친 결함 - 명령행 포커스 상태에서 ENTER가 도구로 가지 않던 문제 - 명령행 문자 입력이 접두사가 같은 명령으로 실행되던 문제 - 명령이 끝나도 입력을 계속 먹던 문제(점 입력 명령만 반복, 나머지는 선택 도구 복귀) - 시퀀스 단계 인덱스 오사용 9건 + 단계 값 종류 검사 추가 - 해치 내부를 클릭해도 선택되지 않던 문제 미반영 8건(맞춤법 검사·꺾기 치수·치수 끊기/재연관/검사 치수·기하공차·지시선 수집· 축척 리스트 편집)은 조사표 `반영` 열과 PLAN.md에 사유를 적었다.
155 lines
3.8 KiB
TypeScript
155 lines
3.8 KiB
TypeScript
/** 특성 팔레트 본문 — 선택 객체의 값을 읽고 바로 고친다 (PROPERTIES) */
|
||
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';
|
||
|
||
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);
|
||
}
|
||
|
||
/** 여러 객체가 값이 다르면 '*가지각색' 대신 첫 객체 값을 보여 준다 (AutoCAD와 같은 관행) */
|
||
export const PropertiesEditor: FC<PropertiesEditorProps> = ({ compact = false }) => {
|
||
const selected = getSelectedEntities();
|
||
const layers = getLayers();
|
||
const first = selected[0];
|
||
|
||
if (!first) {
|
||
return <p className="cad-properties__empty">선택된 객체가 없습니다.</p>;
|
||
}
|
||
|
||
const points = sampleEntityPoints(first);
|
||
const box = first.getBoundingBox();
|
||
|
||
return (
|
||
<div className="cad-properties-editor" data-compact={compact}>
|
||
<div className="cad-properties-editor__title">
|
||
{selected.length === 1 ? first.getType() : `여러 객체 (${selected.length})`}
|
||
</div>
|
||
|
||
<label>
|
||
<span>색상</span>
|
||
<input
|
||
type="color"
|
||
value={first.lineColor}
|
||
onChange={(event) =>
|
||
applyToSelection((entity) => {
|
||
entity.lineColor = event.target.value;
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
|
||
<label>
|
||
<span>선가중치</span>
|
||
<select
|
||
value={first.lineWidth}
|
||
onChange={(event) =>
|
||
applyToSelection((entity) => {
|
||
entity.lineWidth = Number(event.target.value);
|
||
})
|
||
}
|
||
>
|
||
{LINE_WIDTHS.map((width) => (
|
||
<option key={width} value={width}>
|
||
{width}px
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
|
||
<label>
|
||
<span>선종류</span>
|
||
<select
|
||
value={dashToLineType(first.lineDash)}
|
||
onChange={(event) => {
|
||
const dash = LINE_TYPES.find((type) => type.value === event.target.value)?.dash;
|
||
applyToSelection((entity) => {
|
||
entity.lineDash = dash ? [...dash] : undefined;
|
||
});
|
||
}}
|
||
>
|
||
{LINE_TYPES.map((type) => (
|
||
<option key={type.value} value={type.value}>
|
||
{type.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
|
||
<label>
|
||
<span>도면층</span>
|
||
<select
|
||
value={first.layerId}
|
||
onChange={(event) =>
|
||
applyToSelection((entity) => {
|
||
entity.layerId = event.target.value;
|
||
})
|
||
}
|
||
>
|
||
{layers.map((layer) => (
|
||
<option key={layer.id} value={layer.id}>
|
||
{layer.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
|
||
<label>
|
||
<span>투명도</span>
|
||
<input
|
||
type="number"
|
||
min={0}
|
||
max={90}
|
||
step={5}
|
||
value={Math.round((1 - (first.opacity ?? 1)) * 100)}
|
||
onChange={(event) => {
|
||
const percent = Math.min(90, Math.max(0, Number(event.target.value)));
|
||
applyToSelection((entity) => {
|
||
entity.opacity = 1 - percent / 100;
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
|
||
{!compact && (
|
||
<dl className="cad-properties-editor__readout">
|
||
<div>
|
||
<dt>길이</dt>
|
||
<dd>{polylineLength(points).toFixed(3)}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>크기</dt>
|
||
<dd>
|
||
{(box.xmax - box.xmin).toFixed(3)} × {(box.ymax - box.ymin).toFixed(3)}
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt>시작점</dt>
|
||
<dd>
|
||
{points[0] ? `${points[0].x.toFixed(2)}, ${points[0].y.toFixed(2)}` : '-'}
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt>그룹</dt>
|
||
<dd>{first.groupId ? '있음' : '없음'}</dd>
|
||
</div>
|
||
</dl>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|