feat(B07): CAD를 AutoCAD 명령 체계로 재구성한다 (조사표 1·2·3·5절)

저장소 사고로 잃은 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에 사유를 적었다.
This commit is contained in:
2026-08-30 01:34:13 +09:00
parent 60bbe89edb
commit ea9d9685be
88 changed files with 8052 additions and 850 deletions
@@ -0,0 +1,154 @@
/** 특성 팔레트 본문 — 선택 객체의 값을 읽고 바로 고친다 (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>
);
};