파일마다 포맷 폭이 달라(≈80 대 100) 한 줄만 고쳐도 포맷터가 무관한 줄을 대량 재포맷했음. 사용자 지시로 전체를 한 번에 맞춤. 코드 동작 변경 없음 — 포맷만. - 프론트엔드 `.ts/.css/.html` → 저장소 prettier (`.prettierrc`, printWidth 100) - `B07_DesignDetail/openwebcad/**` → 자체 biome (tab 들여쓰기·single quote·lineWidth 100). `biome format` 만 사용 — `biome lint --write` 는 포맷 아닌 코드 수정까지 하므로 제외 - 파이썬 → `ruff format` (엔진 코드는 이미 정합, resources·scratch 스크립트 24개만 변경) 두 포맷터가 서로 되돌리지 않도록 `.prettierignore` 신규 — openwebcad 와 빌드·산출물 폴더를 prettier 대상에서 뺌. `.prettierrc` 에 `endOfLine: "auto"` 추가 — 기본값 `lf` 가 `core.autocrlf=true` 로 받은 CRLF 파일을 매번 전부 다시 써서 `--list-different` 가 실제 포맷 차이를 가리고 있었음. 검증: `tsc --noEmit` 통과(루트·openwebcad 둘 다), pytest 349 passed / 17 skipped / 0 failed, CAD vitest 87건 중 81 passed / 6 failed(laptop-sub 기준선과 동일, 회귀 없음). 포맷터 재실행 시 prettier·biome 모두 변경 0건. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
5.5 KiB
TypeScript
149 lines
5.5 KiB
TypeScript
/** 대칭·정렬·간격띄우기·신축·길이조정 (조사표 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);
|
|
},
|
|
});
|