Files
Aislo/B07_DesignDetail/openwebcad/src/tools/line-tool.ts
T
eomsangdonandClaude Opus 5.5 3cebb37ed0 feat(CAD): 337-3a 나-1 그리기 8 — 오토캐드 물음 차례 · 옵션 · 기본값 · 빈 Enter · Esc · 끝난 뒤
- LINE: 「첫 번째 점 지정」 빈 Enter = 마지막 끝점에서 이음 · U = 마지막 토막(되돌림 한 칸) · 둘째 토막부터 C · Enter · Esc = 끝(토막 남음)
- PLINE(새 pline-tool): 호(A) 접선 호 · 길이(L) · 닫기(C · 호 모드 CL) · 명령 취소(U) · 선(L) · Esc · Enter = 그린 데까지 남김
- RECTANG: 치수(D) 길이 · 폭 기억(RECT.L · RECT.W) → 누른 쪽 사분면 · 없는 옵션 안내
- CIRCLE: 3P · 2P · 지름(D) <2r> · 반지름 기억(CIRCLERAD · 처음 <> 없음) · 한 번 뒤 끝
- POLYGON(새 polygon-tool): 변 수 기억 <4> · 3~1024 정수 · 모서리(E) · 내접/외접 기억 · 숫자 = 아래 변 수평
- ARC(새 arc-tool): 세 점 · 중심(C) · 각도(A) · 끝(E) · 반지름(R)
- ELLIPSE(새 ellipse-tool): 축 두 끝 · 중심(C) · 회전(R) · 다른 축 숫자 = 중심에서 반 길이(N10)
- SPLINE: 다음 점 입력 · U · 넷째 점부터 C · Esc 버림 · DONUT 중심 되풀이(loopFrom)
- 시험 draw-acad 22 · keyboard-chain 옛 선 · 폴리선 줄 두 곳 새 동작으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
2026-10-16 00:16:15 +09:00

179 lines
5.7 KiB
TypeScript

/**
* 선(LINE) — 337 설계 1-1 · 오토캐드 차례.
* 「첫 번째 점 지정:」(빈 Enter = 마지막 선 · 폴리선 · 호 끝점에서) → 「다음 점 지정 또는 [명령 취소(U)]:」 되풀이 ·
* 둘째 토막부터 [닫기(C)/명령 취소(U)]. 토막마다 바로 도면에 · U = 마지막 토막(되돌림 한 칸) · C = 첫 점으로 닫고 끝 ·
* 빈 Enter · Esc = 끝(그린 토막 남음).
*/
import type { Point } from '@flatten-js/core';
import { assign, createMachine } from 'xstate';
import { OPT, type PromptSpec } from '../commands/prompt';
import type { LineEntity } from '../entities/LineEntity';
import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
import {
addEntities,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
undo,
} from '../state';
import { Tool } from '../tools';
import {
END,
firstPointEvents,
lastDrawEnd,
pointEvents,
rememberDrawEnd,
} from './draw/draw-steps';
import { lineEntity } from './factories/entity-factory';
import {
type DrawEvent,
END_STATE,
type PointInputEvent,
type StateEvent,
type ToolContext,
} from './tool.types';
export interface LineContext extends ToolContext {
startPoint: Point | null;
/** 이번 이어 그리기의 첫 점 — 닫기(C) */
firstPoint: Point | null;
/** 이번 이어 그리기에서 그린 구간 — 명령 취소(U) */
segments: LineEntity[];
}
export enum LineState {
INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
}
export enum LineAction {
INIT_LINE_TOOL = 'INIT_LINE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
CONTINUE_FROM_LAST = 'CONTINUE_FROM_LAST',
DRAW_TEMP_LINE = 'DRAW_TEMP_LINE',
DRAW_FINAL_LINE = 'DRAW_FINAL_LINE',
CLOSE_LINE = 'CLOSE_LINE',
UNDO_SEGMENT = 'UNDO_SEGMENT',
}
const START_PROMPT: PromptSpec = { text: '첫 번째 점 지정' };
const canClose = (context: Partial<LineContext>) => (context.segments?.length ?? 0) >= 2;
const nextPrompt = (context: Partial<LineContext>): PromptSpec => ({
text: '다음 점 지정',
options: canClose(context) ? [OPT.CLOSE, OPT.UNDO] : [OPT.UNDO],
});
const startAt = (startPoint: Point) => {
setAngleGuideOriginPoint(startPoint);
return { startPoint, firstPoint: startPoint, segments: [] };
};
export const lineToolStateMachine = createMachine(
{
types: {} as {
context: LineContext;
events: StateEvent;
},
context: {
startPoint: null,
firstPoint: null,
segments: [],
type: Tool.LINE,
},
initial: LineState.INIT,
states: {
[LineState.INIT]: {
always: {
actions: LineAction.INIT_LINE_TOOL,
target: LineState.WAITING_FOR_START_POINT,
},
},
[LineState.WAITING_FOR_START_POINT]: {
meta: { kind: 'point', prompt: START_PROMPT },
on: {
...firstPointEvents(LineAction.RECORD_START_POINT, LineState.WAITING_FOR_END_POINT),
ENTER: {
guard: () => !!lastDrawEnd(),
actions: LineAction.CONTINUE_FROM_LAST,
target: LineState.WAITING_FOR_END_POINT,
},
ESC: { target: END_STATE },
},
},
[LineState.WAITING_FOR_END_POINT]: {
meta: { kind: 'point', prompt: nextPrompt },
on: {
DRAW: { actions: LineAction.DRAW_TEMP_LINE },
...pointEvents(LineAction.DRAW_FINAL_LINE, LineState.WAITING_FOR_END_POINT),
'OPTION.CLOSE': {
guard: 'canClose',
actions: LineAction.CLOSE_LINE,
target: END_STATE,
},
'OPTION.UNDO': [
{
guard: 'hasSegment',
actions: LineAction.UNDO_SEGMENT,
},
// 그린 토막 없음 — 첫 점부터 다시
{ actions: LineAction.INIT_LINE_TOOL, target: LineState.WAITING_FOR_START_POINT },
],
ENTER: { target: END_STATE },
ESC: { target: END_STATE },
},
},
...END,
},
},
{
// 옵션 사건(`OPTION.*`)은 이름 붙인 guard 로 — 글자 사건 꼴이라 줄 안 guard 는 타입이 안 맞음
guards: {
canClose: ({ context }) => canClose(context),
hasSegment: ({ context }) => context.segments.length > 0,
},
actions: {
[LineAction.INIT_LINE_TOOL]: assign(() => {
setShouldDrawHelpers(true);
setSelectedEntityIds([]);
setGhostHelperEntities([]);
setAngleGuideOriginPoint(null);
return { startPoint: null, firstPoint: null, segments: [] };
}),
[LineAction.RECORD_START_POINT]: assign(({ event }) =>
startAt(getPointFromEvent(null, event as PointInputEvent))
),
[LineAction.CONTINUE_FROM_LAST]: assign(() => startAt(lastDrawEnd() as Point)),
[LineAction.DRAW_TEMP_LINE]: ({ context, event }) => {
const start = context.startPoint as Point;
setGhostHelperEntities([lineEntity(start, getPointFromEvent(start, event as DrawEvent))]);
},
[LineAction.DRAW_FINAL_LINE]: assign(({ context, event }) => {
const start = context.startPoint as Point;
const endPoint = getPointFromEvent(start, event as PointInputEvent);
const segment = lineEntity(start, endPoint);
addEntities([segment], true);
rememberDrawEnd(endPoint);
setGhostHelperEntities([]);
setAngleGuideOriginPoint(endPoint);
return { startPoint: endPoint, segments: [...context.segments, segment] };
}),
[LineAction.CLOSE_LINE]: ({ context }) => {
const first = context.firstPoint as Point;
addEntities([lineEntity(context.startPoint as Point, first)], true);
rememberDrawEnd(first);
},
[LineAction.UNDO_SEGMENT]: assign(({ context }) => {
const last = context.segments[context.segments.length - 1];
undo(); // 토막 하나 = 되돌림 한 칸
const startPoint = last.getStartPoint();
rememberDrawEnd(startPoint);
setGhostHelperEntities([]);
setAngleGuideOriginPoint(startPoint);
return { startPoint, segments: context.segments.slice(0, -1) };
}),
},
}
);