Files
Aislo/B07_DesignDetail/openwebcad/src/tools/move-tool.ts
T
eomsangdonandClaude Opus 5.5 d35bdfa69d feat(CAD): 337-5 라 옮기기 무리 1 단계 — MOVE · ROTATE · SCALE 오토캐드 물음 차례
- MOVE: 기준점 [변위(D)] <변위> · 두 번째 점 <첫 번째 점을 변위로 사용> · 변위 기억(MOVE.DISP) · 한 번 뒤 끝 · Esc 제자리 끝
- ROTATE · SCALE: 공용 틀(pivot-tool) — 누름 한 번 = 값 · 복사(C) · 참조(R) → 새 값 [점(P)] · 각 · 비율 기억 · 0 이하 축척 거절
- 공용 고르기 상태(transform.helpers) — PICKFIRST · 빈 고르기 Enter = 끝 · OPTION.* 넘김 · 원본은 확정까지 도면에 그대로(N1 · N2)
- 시험 transform-acad(15) · scale-tool 단위 시험 새 상태 이름

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

182 lines
6.0 KiB
TypeScript

/**
* MOVE(337 라 · 설계 1-3) — 「객체 선택:」 → 「기준점 지정 또는 [변위(D)] <변위>:」 →
* 「두 번째 점 지정 또는 <첫 번째 점을 변위로 사용>:」 · D → 「변위 지정 <0.0000, 0.0000>:」.
* 한 번 뒤 끝 · Esc = 제자리 끝 · 변위 기억(MOVE.DISP). STRETCH 도 이 틀(`createDisplaceTool`).
*/
import type { Point } from '@flatten-js/core';
import { assign, createMachine } from 'xstate';
import { OPT, type PromptSpec, remember } from '../commands/prompt';
import type { Entity } from '../entities/Entity';
import { getPointFromEvent } from '../helpers/get-point-from-event';
import { setAngleGuideOriginPoint, setGhostHelperEntities, setShouldDrawHelpers } from '../state';
import { Tool } from '../tools';
import {
type DrawEvent,
END_STATE,
type PointInputEvent,
type StateEvent,
type ToolContext,
} from './tool.types';
import {
type PickedContext,
changedCopies,
endState,
guideLine,
replaceInPlace,
selectionState,
startState,
} from './transform.helpers';
export enum MoveState {
INIT = 'INIT',
WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION',
WAITING_FOR_BASE_POINT = 'WAITING_FOR_BASE_POINT',
WAITING_FOR_SECOND_POINT = 'WAITING_FOR_SECOND_POINT',
WAITING_FOR_DISPLACEMENT = 'WAITING_FOR_DISPLACEMENT',
}
export interface DisplaceContext extends ToolContext, PickedContext {
base: Point | null;
/** 고르기 중 마지막 걸침 창(STRETCH) */
window: [Point, Point] | null;
}
export interface DisplaceConfig {
tool: Tool;
/** 변위 기억 열쇠 */
remember: string;
/** 변위만큼 바꾼 사본(id 그대로 · 안 바뀐 것은 빼도 됨) */
change: (context: DisplaceContext, dx: number, dy: number) => Entity[];
/** 고르기 누름 엿봄(걸침 창 기억) */
watch?: unknown;
/** 시작 기록 줄 */
intro?: string;
}
type Ctx = { context: DisplaceContext; event: StateEvent };
export const BASE_PROMPT: PromptSpec = {
text: '기준점 지정',
options: [OPT.DISPLACEMENT],
default: { kind: 'option', key: OPT.DISPLACEMENT.key },
};
export const SECOND_PROMPT: PromptSpec = {
// 옵션 없이 「또는 <…>」 — 오토캐드 줄 그대로
text: '두 번째 점 지정 또는',
default: { kind: 'enter', label: '첫 번째 점을 변위로 사용' },
};
export function createDisplaceTool(config: DisplaceConfig) {
const S = MoveState;
const DISP_PROMPT: PromptSpec = {
text: '변위 지정',
default: { kind: 'value', remember: config.remember, initial: { x: 0, y: 0 } },
};
const commit = (context: DisplaceContext, dx: number, dy: number) => {
replaceInPlace(config.change(context, dx, dy));
remember(config.remember, { x: dx, y: dy });
};
const toward = ({ context, event }: Ctx) =>
getPointFromEvent(context.base, event as PointInputEvent);
const commitSecond = (args: Ctx) => {
const base = args.context.base as Point;
const dest = toward(args);
commit(args.context, dest.x - base.x, dest.y - base.y);
};
// 첫 점 좌표가 곧 변위(빈 Enter) · D 변위 물음의 점
const commitPointAsVector = (point: Point, context: DisplaceContext) =>
commit(context, point.x, point.y);
const done = (actions: unknown) => ({ actions, target: END_STATE });
const pointEvents = (actions: unknown) => ({
MOUSE_CLICK: done(actions),
ABSOLUTE_POINT_INPUT: done(actions),
RELATIVE_POINT_INPUT: done(actions),
NUMBER_INPUT: done(actions),
});
// biome-ignore lint/suspicious/noExplicitAny: 공용 상태 조각(여러 도구 공통)을 섞어 만든다
const states: Record<string, any> = {
[S.INIT]: {
...startState(S.WAITING_FOR_BASE_POINT),
exit: () => {
if (config.intro) {
const intro = config.intro;
void import('../commands/run-command').then(({ logLine }) => logLine(intro));
}
},
},
[S.WAITING_FOR_SELECTION]: selectionState(S.WAITING_FOR_BASE_POINT, config.watch),
[S.WAITING_FOR_BASE_POINT]: {
meta: { kind: 'point', prompt: BASE_PROMPT },
entry: () => setShouldDrawHelpers(true),
on: {
MOUSE_CLICK: {
actions: assign(({ event }) => {
const base = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(base);
return { base };
}),
target: S.WAITING_FOR_SECOND_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: assign(({ event }) => {
const base = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(base);
return { base };
}),
target: S.WAITING_FOR_SECOND_POINT,
},
'OPTION.DISPLACEMENT': { target: S.WAITING_FOR_DISPLACEMENT },
ESC: { target: END_STATE },
},
},
[S.WAITING_FOR_SECOND_POINT]: {
meta: { kind: 'point', prompt: SECOND_PROMPT },
on: {
DRAW: {
actions: (args: Ctx) => {
const base = args.context.base as Point;
const dest = getPointFromEvent(base, args.event as DrawEvent);
setGhostHelperEntities([
guideLine(base, dest),
...config.change(args.context, dest.x - base.x, dest.y - base.y),
]);
},
},
...pointEvents(commitSecond),
ENTER: done(({ context }: Ctx) => commitPointAsVector(context.base as Point, context)),
ESC: { target: END_STATE },
},
},
[S.WAITING_FOR_DISPLACEMENT]: {
meta: { kind: 'point', prompt: DISP_PROMPT },
entry: () => setAngleGuideOriginPoint(null),
on: {
MOUSE_CLICK: done(({ context, event }: Ctx) =>
commitPointAsVector(getPointFromEvent(null, event as PointInputEvent), context)
),
ABSOLUTE_POINT_INPUT: done(({ context, event }: Ctx) =>
commitPointAsVector(getPointFromEvent(null, event as PointInputEvent), context)
),
ESC: { target: END_STATE },
},
},
[END_STATE]: endState,
};
return createMachine({
types: {} as { context: DisplaceContext; events: StateEvent },
context: { originals: [], base: null, window: null, type: config.tool },
initial: S.INIT,
states,
});
}
export const moveToolStateMachine = createDisplaceTool({
tool: Tool.MOVE,
remember: 'MOVE.DISP',
change: (context, dx, dy) => changedCopies(context.originals, (entity) => entity.move(dx, dy)),
});