- 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
182 lines
6.0 KiB
TypeScript
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)),
|
|
});
|