- 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
179 lines
5.7 KiB
TypeScript
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) };
|
|
}),
|
|
},
|
|
}
|
|
);
|