- 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
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
/**
|
|
* 옮기기 무리 공용(337 라 · 설계 1-3 · 2-14) — 시작(PICKFIRST) · 고르기 물음 · 끝 · 제자리 바꿔 끼움 · 사본 · 그림자 · 각 · 거리.
|
|
* 원본은 확정 때까지 도면에 그대로 둠 — Esc · 딴 명령 · 되돌림(N1)에도 제자리 · 두 벌 없음.
|
|
*/
|
|
import { Point } from '@flatten-js/core';
|
|
import { assign, sendTo } from 'xstate';
|
|
import { GUIDE_LINE_COLOR, GUIDE_LINE_STYLE, GUIDE_LINE_WIDTH } from '../App.consts';
|
|
import { SELECT_OBJECTS, rememberSelection } from '../commands/prompt';
|
|
import type { Entity } from '../entities/Entity';
|
|
import { LineEntity } from '../entities/LineEntity';
|
|
import {
|
|
addEntities,
|
|
getActiveLayerId,
|
|
getEntities,
|
|
getSelectedEntities,
|
|
setAngleGuideOriginPoint,
|
|
setEntities,
|
|
setGhostHelperEntities,
|
|
setSelectedEntityIds,
|
|
setShouldDrawHelpers,
|
|
} from '../state';
|
|
import { cloneKeepingLook } from './move-tool.helpers';
|
|
import { selectToolStateMachine } from './select-tool';
|
|
import { END_STATE } from './tool.types';
|
|
|
|
export const SELECT_STATE = 'WAITING_FOR_SELECTION';
|
|
export const SELECT_CHILD = 'selectInsideTransform';
|
|
|
|
export interface PickedContext {
|
|
/** 고른 원본(도면에 그대로 있음) */
|
|
originals: Entity[];
|
|
}
|
|
|
|
export type Change = (entity: Entity) => void;
|
|
|
|
/** 고른 것 받음 — 고르기 이전(P) 열쇠 */
|
|
const takeSelection = assign(() => {
|
|
const picked = getSelectedEntities();
|
|
rememberSelection(picked.map((entity) => entity.id));
|
|
return { originals: picked };
|
|
});
|
|
|
|
/** 끝 정리 — 그림자 · 기준점 · 고름 풂 */
|
|
export const endClean = () => {
|
|
setGhostHelperEntities([]);
|
|
setAngleGuideOriginPoint(null);
|
|
setSelectedEntityIds([]);
|
|
};
|
|
|
|
/** 시작 — 고른 것 있으면 받고 next 로(PICKFIRST) · 없으면 고르기 */
|
|
export const startState = (next: string) => ({
|
|
entry: () => {
|
|
setShouldDrawHelpers(false);
|
|
setGhostHelperEntities([]);
|
|
setAngleGuideOriginPoint(null);
|
|
},
|
|
always: [
|
|
{ guard: () => getSelectedEntities().length > 0, actions: takeSelection, target: next },
|
|
{ target: SELECT_STATE },
|
|
],
|
|
});
|
|
|
|
/**
|
|
* 고르기 물음(설계 2-14 ④) — 자식 선택 도구에 넘김(ALL · L · P 포함) · 고른 것 없이 Enter = 끝.
|
|
* `watch` = 넘기기 전 누름 엿보는 액션(자식은 아직 그 누름 전 · STRETCH 걸침 창).
|
|
*/
|
|
export const selectionState = (next: string, watch?: unknown) => {
|
|
const send = sendTo(SELECT_CHILD, ({ event }) => event);
|
|
const forward = { actions: send };
|
|
return {
|
|
meta: { kind: 'selection', prompt: SELECT_OBJECTS },
|
|
entry: () => setShouldDrawHelpers(false),
|
|
invoke: {
|
|
id: SELECT_CHILD,
|
|
src: selectToolStateMachine,
|
|
onDone: [
|
|
{ guard: () => getSelectedEntities().length === 0, target: END_STATE },
|
|
{ actions: takeSelection, target: next },
|
|
],
|
|
},
|
|
on: {
|
|
MOUSE_CLICK: watch ? { actions: [watch, send] } : forward,
|
|
ENTER: forward,
|
|
DRAW: forward,
|
|
'OPTION.*': forward,
|
|
ESC: { target: END_STATE },
|
|
},
|
|
};
|
|
};
|
|
|
|
/** 끝 상태 — 들면 쉬는 상태로(end-command) */
|
|
export const endState = { type: 'final' as const, entry: endClean };
|
|
|
|
/** 바꾼 사본(id 그대로) — 제자리 바꿔 끼울 것 */
|
|
export const changedCopies = (originals: Entity[], change: Change): Entity[] =>
|
|
originals.map((entity) => {
|
|
const copy = cloneKeepingLook(entity, true);
|
|
change(copy);
|
|
return copy;
|
|
});
|
|
|
|
/** 원본 자리에 바꾼 것 끼움 — 순서 · id 그대로 · 되돌림 한 칸 */
|
|
export function replaceInPlace(copies: Entity[]) {
|
|
if (!copies.length) return;
|
|
const byId = new Map(copies.map((copy) => [copy.id, copy]));
|
|
setEntities(
|
|
getEntities().map((entity) => byId.get(entity.id) ?? entity),
|
|
true
|
|
);
|
|
}
|
|
|
|
/** 새 사본 더함(새 id) — 되돌림 한 칸 */
|
|
export function addCopies(originals: Entity[], change: Change): Entity[] {
|
|
const copies = originals.map((entity) => {
|
|
const copy = cloneKeepingLook(entity);
|
|
change(copy);
|
|
return copy;
|
|
});
|
|
if (copies.length) addEntities(copies, true);
|
|
return copies;
|
|
}
|
|
|
|
/** 기준점 → 커서 점선 */
|
|
export function guideLine(from: Point, to: Point): LineEntity {
|
|
const line = new LineEntity(getActiveLayerId(), from, to);
|
|
line.lineColor = GUIDE_LINE_COLOR;
|
|
line.lineWidth = GUIDE_LINE_WIDTH;
|
|
line.lineDash = GUIDE_LINE_STYLE;
|
|
return line;
|
|
}
|
|
|
|
/** 미리보기 — 원본은 두고 바꾼 그림자만 */
|
|
export const showGhosts = (originals: Entity[], change: Change, ...extra: Entity[]) =>
|
|
setGhostHelperEntities([...extra, ...changedCopies(originals, change)]);
|
|
|
|
/** a → b 방향 각(도 · 동쪽 0 · 반시계 +) */
|
|
export const angleOf = (a: Point, b: Point): number =>
|
|
(Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI;
|
|
|
|
export const distanceOf = (a: Point, b: Point): number => Math.hypot(b.x - a.x, b.y - a.y);
|
|
|
|
export const toRadians = (degrees: number) => (degrees * Math.PI) / 180;
|
|
|
|
/** 변위 → 점(좌표 꼴) */
|
|
export const asVector = (from: Point, to: Point) => new Point(to.x - from.x, to.y - from.y);
|