refactor(CAD): 337-6 가 끝 몫 — 옛 OFFSET · LENGTHEN · 옛 꼴 통과 지움 · ALL · L · P 실제 고름 시험 · 안내 글

- transform-acad: 고르기 물음 ALL · L · P = 「n개를 찾음, 총 m개」 · 다시 「객체 선택:」 · 빈 Enter = 고르기 끝(설계 1-4 · 2-15)
- transform-tools: 안 쓰는 옛 OFFSET · LENGTHEN 머신 지움(새 offset-tool · lengthen-tool 이 대신)
- prompt-input · prompt · sequence-tool: 옛 꼴 통과(meta.options 글 배열 · TEXT_INPUT 대문자 통과 · chainFromLastPoint) 지움
- prompt-contract: 옛 꼴 안 쓰는지 지키는 시험 더함
- 안내 글(설계 4 장 끝): 오른쪽 단추 · 휠 두 번 · CAD 키 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
2026-10-16 00:34:02 +09:00
co-authored by Claude Opus 5.5
parent 746c70fcce
commit 933c4c5d71
7 changed files with 39 additions and 85 deletions
@@ -47,8 +47,6 @@ export interface PromptMeta {
kind?: StepKind;
prompt?: PromptLike;
instructions?: string;
/** 옛 꼴 — 점 물음이 받는 옵션 글자(대문자) */
options?: string[];
}
/** 함수 꼴 물음을 지금 context 로 풂 */
@@ -147,14 +147,12 @@ export interface CurrentPrompt {
spec: PromptSpec | null;
/** 그린 한 줄(새 꼴) 또는 옛 instructions */
line: string | null;
/** 옛 꼴 — 점 물음이 받는 옵션 글자 */
legacyOptions: string[];
}
export function currentPrompt(): CurrentPrompt {
const sub = subPrompt();
if (sub) {
return { kind: sub.kind, spec: sub.prompt, line: renderPrompt(sub.prompt, sub.kind), legacyOptions: [] };
return { kind: sub.kind, spec: sub.prompt, line: renderPrompt(sub.prompt, sub.kind) };
}
const meta = activeMeta();
const spec = resolvePrompt(meta?.prompt, activeSnapshot()?.context);
@@ -163,7 +161,6 @@ export function currentPrompt(): CurrentPrompt {
kind,
spec,
line: spec ? renderPrompt(spec, meta?.kind) : (meta?.instructions ?? null),
legacyOptions: meta?.options ?? [],
};
}
@@ -276,12 +273,6 @@ function numberProblem(value: number, rule: PromptSpec['number']): string | null
// ---- 사다리(설계 2-5) ----
/** 옛 꼴 통과 — 새 물음이 없는 상태가 TEXT_INPUT 을 받고 숫자는 안 받음 */
const legacyTakesText = (): boolean => {
const on = (flatConfig()?.on ?? {}) as Record<string, unknown>;
return !!on.TEXT_INPUT && !on.NUMBER_INPUT;
};
/** ② 빈 Enter · Space · 우클릭 */
function routeEmpty(prompt: CurrentPrompt) {
const lockedLength = getLockedLength();
@@ -400,17 +391,8 @@ export function routeTyped(raw: string): void {
notice(`「${hit.absent.label}(${hit.absent.kw})」 옵션은 이 CAD 에 없음.`);
return;
}
const upper = normalizeKeyword(text);
if (!prompt.spec && prompt.legacyOptions.includes(upper)) {
send({ type: ActorEvent.TEXT_INPUT, value: upper }); // 옛 꼴 통과
return;
}
if (prompt.kind === 'point') return routePointText(text, prompt);
if (prompt.kind === 'number') return routeNumber(text, prompt);
if (!prompt.spec && legacyTakesText()) {
send({ type: ActorEvent.TEXT_INPUT, value: upper }); // 옛 꼴 통과
return;
}
reject(prompt); // ⑦ ⑩
}
@@ -118,8 +118,6 @@ export interface SequenceToolConfig {
pickfirst?: boolean;
/** 옛 꼴 — true = `loopFrom: 0` · false = 끝 */
restart?: boolean;
/** 옛 꼴 — 확정 뒤 마지막 점을 첫 값으로 이어받아 계속 그린다 */
chainFromLastPoint?: boolean;
/** 점 입력 중 스냅·각도 가이드 사용 여부 (기본 true) */
helpers?: boolean;
/** ESC 로 확정 없이 그만둘 때 */
@@ -201,9 +199,7 @@ export function createSequenceTool(config: SequenceToolConfig) {
? indexOf(config.loopFrom)
: config.restart === true
? 0
: config.chainFromLastPoint && config.restart !== false
? 1
: null;
: null;
for (const step of steps) if (step.next !== undefined) indexOf(step.next);
const isRepeat = (index: number) => !!config.repeatLastStep && index === lastIndex;
@@ -375,18 +371,16 @@ export function createSequenceTool(config: SequenceToolConfig) {
setGhostHelperEntities([]);
};
/** 확정 뒤 되풀이 — loopFrom 앞 값만 둠(옛 chainFromLastPoint = 마지막 점 이어받음) */
/** 확정 뒤 되풀이 — loopFrom 앞 값만 둠 */
const loopAgain = assign(({ context }: Ctx): Partial<SequenceContext> => {
const from = loopIndex as number;
const tail = lastPoint(context.values);
const chain = config.chainFromLastPoint && config.loopFrom === undefined && tail;
const keep = context.values.map((_, index) => chain || context.stepOf[index] < from);
const values = chain ? [tail] : context.values.filter((_, index) => keep[index]);
const keep = context.values.map((_, index) => context.stepOf[index] < from);
const values = context.values.filter((_, index) => keep[index]);
const next = {
...context,
values,
picks: chain ? [tail] : context.picks.filter((_, index) => keep[index]),
stepOf: chain ? [0] : context.stepOf.filter((_, index) => keep[index]),
picks: context.picks.filter((_, index) => keep[index]),
stepOf: context.stepOf.filter((_, index) => keep[index]),
closed: false,
loops: context.loops + 1,
};
@@ -1,6 +1,5 @@
/** 대칭·정렬·간격띄우기·신축·길이조정 (조사표 2절) */
/** 대칭·정렬·신축 (조사표 2절) */
import { Box, Point, Polygon } from '@flatten-js/core';
import { toast } from 'react-toastify';
import { type AnyActorRef, assign } from 'xstate';
import { OPT } from '../../commands/prompt';
import type { Entity } from '../../entities/Entity';
@@ -21,7 +20,7 @@ import { createDisplaceTool } from '../move-tool';
import { SelectState } from '../select-tool';
import type { MouseClickEvent, StateEvent } from '../tool.types';
import { SELECT_CHILD } from '../transform.helpers';
import { copyStyle, lengthenLine, offsetEntity } from './modify.helpers';
import { copyStyle } from './modify.helpers';
/** 원본 특성을 유지한 복사본 */
const cloneStyled = (entity: Entity): Entity => copyStyle(entity, entity.clone());
@@ -110,23 +109,6 @@ export const alignToolStateMachine = createSequenceTool({
},
});
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);
},
});
/** 바꾼 것을 원본 자리에 — id · 묶음 · 꼴 그대로 */
const inPlaceOf = <T extends Entity>(source: Entity, made: T): T => {
copyStyle(source, made);
@@ -208,25 +190,3 @@ export const stretchToolStateMachine = createDisplaceTool({
return context.originals.flatMap((entity) => stretchEntity(entity, box, dx, dy) ?? []);
},
});
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);
},
});
@@ -62,4 +62,15 @@ describe("물음 약속(2-16)", () => {
}
expect(problems).toEqual([]);
});
it("옛 꼴 통과 안 씀 — 옵션 글 배열 없음 · 숫자 없이 TEXT_INPUT 만 받는 상태 없음(글 물음 빼고)", () => {
const problems: string[] = [];
for (const { where, meta, on } of rows) {
if ((meta as { options?: unknown } | undefined)?.options)
problems.push(`${where}: meta.options`);
if (on.TEXT_INPUT && !on.NUMBER_INPUT && meta?.kind !== "text")
problems.push(`${where}: TEXT_INPUT 만`);
}
expect(problems).toEqual([]);
});
});
+15 -6
View File
@@ -621,14 +621,23 @@ describe("PEDIT 고르기 · 고르기 공용(2-14 ④ · 2-15)", () => {
expect(entities()).toHaveLength(1);
});
it("고르기 물음의 ALL · L · P 는 거절 없이 선택 도구로 넘어감", () => {
it("고르기 물음의 ALL · L · P = 실제 고름 · 「n개를 찾음」 · 다시 「객체 선택:」 · 빈 Enter = 고르기 끝", () => {
line("0,0", "10,0");
line("0,20", "10,20");
type("m");
for (const word of ["all", "l", "p"]) {
type(word);
expect(noticeLine()).toBeNull();
expect(activeTool()).toBe(Tool.MOVE);
}
type("all");
expect(noticeLine()).toBe("2개를 찾음, 총 2개");
expect(promptLine()).toBe("객체 선택:");
expect(activeTool()).toBe(Tool.MOVE);
type("l");
expect(noticeLine()).toBe("1개를 찾음, 총 2개");
expect(promptLine()).toBe("객체 선택:");
type("p");
expect(noticeLine()).toMatch(/^\d+개를 찾음, 총 2개$/);
expect(promptLine()).toBe("객체 선택:");
enter();
expect(activeTool()).toBe(Tool.MOVE);
expect(promptLine()).toBe("기준점 지정 또는 [변위(D)] <변위>:");
});
const selectTakesKeywords = !!(
+4 -4
View File
@@ -121,8 +121,8 @@ export const ui_locales_common = {
App_Settings_Rule_Pan: ["화면 옮기기 = 가운데 단추 끌기", "Pan = drag with the middle button"],
App_Settings_Rule_Pick: ["고르기 = 왼쪽 단추", "Select = left button"],
App_Settings_Rule_Right: [
"오른쪽 단추 = 메뉴 · CAD 에서는 Enter",
"Right button = menu · Enter in CAD",
"오른쪽 단추 = 메뉴 · CAD 에서는 명령 중 Enter · 쉴 때 반복 · Shift = 스냅 메뉴 · CAD 휠 두 번 = 범위",
"Right button = menu · in CAD Enter during a command · repeat when idle · Shift = snap menu · CAD wheel double-click = extents",
],
App_Settings_Rule_Pinch: [
"트랙패드 · 손가락 벌림 = 늘 확대(휠 방향과 상관없음)",
@@ -164,8 +164,8 @@ export const ui_locales_common = {
],
App_Settings_Keys_Cad: ["CAD", "CAD"],
App_Settings_Keys_Cad_Keys: [
"F3 스냅 · F7 그리드 · F8 직교 · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S(M02) · Delete · Esc · Enter · Space = Enter · 방향키 = 화면 옮김 · 글자 = 명령",
"F3 snap · F7 grid · F8 ortho · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S (M02) · Delete · Esc · Enter · Space = Enter · arrows = pan · letters = command",
"F3 스냅 · F7 그리드 · F8 직교 · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S(M02) · Delete · Esc · Enter · Space = Enter · ↑↓ = 기록 · 최근 값 · Ctrl+C · X · 1 · ← → · Shift+↑↓ = 화면 옮김 · 글자 = 명령",
"F3 snap · F7 grid · F8 ortho · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S (M02) · Delete · Esc · Enter · Space = Enter · ↑↓ = history · recent values · Ctrl+C · X · 1 · ← → · Shift+↑↓ = pan · letters = command",
],
App_Settings_Keys_Table: ["B08 · B09 표", "B08 · B09 tables"],
App_Settings_Keys_Table_Keys: [