/** 대칭·정렬·간격띄우기·신축·길이조정 (조사표 2절) */ import { Point } from '@flatten-js/core'; import { toast } from 'react-toastify'; import type { Entity } from '../../entities/Entity'; import { LineEntity } from '../../entities/LineEntity'; import { addEntities, deleteEntities, getActiveLayerId, setSelectedEntityIds } from '../../state'; import { Tool } from '../../tools'; import { createSequenceTool } from '../factories/sequence-tool'; import { copyStyle, lengthenLine, offsetEntity } from './modify.helpers'; /** 원본 특성을 유지한 복사본 */ const cloneStyled = (entity: Entity): Entity => copyStyle(entity, entity.clone()); export const mirrorToolStateMachine = createSequenceTool({ tool: Tool.MIRROR, steps: [ { kind: 'selection', instructions: '대칭 복사할 객체를 선택한 뒤 ENTER.' }, { kind: 'point', instructions: '대칭축의 첫 점을 지정하십시오.' }, { kind: 'point', instructions: '대칭축의 두 번째 점을 지정하십시오.' }, ], preview: (input) => { const points = input.points(); if (points.length !== 1) return []; return [new LineEntity(getActiveLayerId(), points[0], input.cursor)]; }, commit: (input) => { const [first, second] = input.points(); const axis = new LineEntity(getActiveLayerId(), first, second); const mirrored = input.entities(0).map((entity) => { const copy = cloneStyled(entity); copy.mirror(axis); return copy; }); if (mirrored.length) addEntities(mirrored, true); setSelectedEntityIds([]); }, }); export const alignToolStateMachine = createSequenceTool({ tool: Tool.ALIGN, steps: [ { kind: 'selection', instructions: '정렬할 객체를 선택한 뒤 ENTER.' }, { kind: 'point', instructions: '첫 번째 원본점을 지정하십시오.' }, { kind: 'point', instructions: '첫 번째 대상점을 지정하십시오.' }, { kind: 'point', instructions: '두 번째 원본점을 지정하십시오.' }, { kind: 'point', instructions: '두 번째 대상점을 지정하십시오.' }, ], commit: (input) => { const [source1, target1, source2, target2] = input.points(); const sourceAngle = Math.atan2(source2.y - source1.y, source2.x - source1.x); const targetAngle = Math.atan2(target2.y - target1.y, target2.x - target1.x); const rotation = targetAngle - sourceAngle; // 원본을 그대로 두고 사본을 변환해 교체한다 (실행취소가 원본을 되살릴 수 있어야 한다) const originals = input.entities(0); const aligned = originals.map((entity) => { const copy = cloneStyled(entity); copy.move(target1.x - source1.x, target1.y - source1.y); copy.rotate(target1, rotation); return copy; }); if (originals.length) { deleteEntities(originals, false); addEntities(aligned, true); } setSelectedEntityIds([]); }, }); export const offsetToolStateMachine = createSequenceTool({ tool: Tool.OFFSET, steps: [ { kind: 'number', instructions: '간격띄우기 거리를 입력하십시오 <1>.', defaultValue: 1 }, { kind: 'entity', instructions: '간격띄우기할 객체를 선택하십시오.' }, { kind: 'point', instructions: '간격을 띄울 방향의 점을 지정하십시오.' }, ], commit: (input) => { const result = offsetEntity(input.entity(1), input.number(0), input.point(2)); if (!result) { toast.warn('이 객체는 지정한 거리로 간격을 띄울 수 없습니다.'); return; } addEntities([result], true); }, }); export const stretchToolStateMachine = createSequenceTool({ tool: Tool.STRETCH, steps: [ { kind: 'selection', instructions: '신축할 객체를 선택한 뒤 ENTER.' }, { kind: 'point', instructions: '기준점을 지정하십시오.' }, { kind: 'point', instructions: '이동할 위치를 지정하십시오.' }, ], commit: (input) => { const [base, destination] = input.points(); const dx = destination.x - base.x; const dy = destination.y - base.y; const replaced: Entity[] = []; const removed: Entity[] = []; for (const entity of input.entities(0)) { const shape = entity.getShape(); // 선은 기준점에 가까운 끝점만 끌어당긴다. 그 밖의 객체는 통째로 옮긴다. if (shape && 'start' in shape && 'end' in shape) { const start = (shape as { start: Point }).start; const end = (shape as { end: Point }).end; const moveStart = start.distanceTo(base)[0] < end.distanceTo(base)[0]; const newLine = new LineEntity( entity.layerId, moveStart ? new Point(start.x + dx, start.y + dy) : start, moveStart ? end : new Point(end.x + dx, end.y + dy) ); replaced.push(copyStyle(entity, newLine)); removed.push(entity); } else { const copy = cloneStyled(entity); copy.move(dx, dy); replaced.push(copy); removed.push(entity); } } if (removed.length) deleteEntities(removed, false); addEntities(replaced, true); setSelectedEntityIds([]); }, }); export const lengthenToolStateMachine = createSequenceTool({ tool: Tool.LENGTHEN, steps: [ { kind: 'entity', instructions: '길이를 바꿀 선을 늘릴 쪽 끝 근처에서 선택하십시오.' }, { kind: 'number', instructions: '증분 길이를 입력하십시오 (음수는 단축) <10>.', defaultValue: 10, }, ], commit: (input) => { const entity = input.entity(0); const changed = lengthenLine(entity, input.number(1), input.pick(0)); if (!changed) { toast.warn('선만 길이를 조정할 수 있습니다.'); return; } deleteEntities([entity], false); addEntities([changed], true); }, });