feat(CAD): 337-3b 나-2 구성선 · 글자 · 해치 — XLINE · TEXT · MTEXT · HATCH 오토캐드 물음 차례
- XLINE 「점 지정 또는 [수평(H)/수직(V)/각도(A)]:」 → 「통과점을 지정:」 되풀이 · 각도 기억(XLINE.ANGLE) · 이등분 · 간격띄우기 = 없는 옵션 안내 · 빈 Enter · Esc = 끝(그은 선 남음) - RAY 통과점 되풀이(loopFrom) - TEXT 시작점 → 높이 <TEXTSIZE> → 회전 각 <TEXT.ANGLE> → 줄마다 「문자 입력:」(Enter = 아래 새 줄 · 빈 줄 = 끝) · 시작점 Enter = 마지막 글 아래 이어 씀(높이 · 각 안 물음) - MTEXT 첫 구석 → 「반대 구석 지정 또는 [높이(H)/폭(W)]:」 → 글(\P 줄바꿈) · 상자 왼쪽 위에서 · 줄 감기 없음 - HATCH 「내부 점 선택 또는 [객체 선택(S)/명령 취소(U)]:」 되풀이 · 품은 닫힌 객체 중 가장 작은 경계 · 빈 Enter = 만듦 · Esc = 안 만듦 · 손 머신(엔진은 직교가 둘째 내부 점을 끌어감) - 시험 resources/tester/cad/text-acad.test.ts 12 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/** 문자 명령 — 여러 줄 문자·단일 행 문자·편집·찾기 (조사표 5절 문자 패널) */
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { Point, Vector } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getAnnotationScale } from '../../commands/dim-settings';
|
||||
import { type OptionSpec, type PromptSpec, recall } from '../../commands/prompt';
|
||||
import { EntityName } from '../../entities/Entity';
|
||||
import type { Entity } from '../../entities/Entity';
|
||||
import type { TextEntity } from '../../entities/TextEntity';
|
||||
import {
|
||||
addEntities,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
setEntities,
|
||||
} from '../../state';
|
||||
import { Tool } from '../../tools';
|
||||
import { textEntity } from '../factories/entity-factory';
|
||||
import { polyLineEntity, textEntity } from '../factories/entity-factory';
|
||||
import { createSequenceTool } from '../factories/sequence-tool';
|
||||
|
||||
/** AutoCAD 여러 줄 문자의 줄바꿈 표기(\P)와 \n을 모두 받는다 */
|
||||
@@ -24,42 +24,156 @@ const splitLines = (text: string): string[] =>
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
function buildTextLines(lines: string[], basePoint: Point): Entity[] {
|
||||
const lineHeight = getActiveTextStyle().fontSize * getAnnotationScale() * 1.35;
|
||||
const groupId = lines.length > 1 ? crypto.randomUUID() : undefined;
|
||||
return lines.map((line, index) => {
|
||||
const entity = textEntity(line, new Point(basePoint.x, basePoint.y - lineHeight * index), {
|
||||
textAlign: 'left',
|
||||
fontSize: getActiveTextStyle().fontSize * getAnnotationScale(),
|
||||
});
|
||||
entity.groupId = groupId;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
/** 줄 간격 = 글자 높이 × 1.35 */
|
||||
const LINE_GAP = 1.35;
|
||||
const opt = (key: string, kw: string, label: string): OptionSpec => ({ key, kw, en: key, label });
|
||||
/** 지금 글자 크기 — TEXTSIZE 처음 값 */
|
||||
const textSize = () => getActiveTextStyle().fontSize * getAnnotationScale();
|
||||
const TEXT_HEIGHT = (): PromptSpec => ({
|
||||
text: '높이 지정',
|
||||
default: { kind: 'value', remember: 'TEXTSIZE', initial: textSize() },
|
||||
number: { positive: true },
|
||||
});
|
||||
/** 글 물음 — 빈 Enter = 끝(기본은 안 그림) */
|
||||
const typing = (text: string): PromptSpec => ({ text, default: { kind: 'option', key: 'DONE' } });
|
||||
|
||||
/** 한 줄 글 — 높이 · 회전 각(도) */
|
||||
const placeText = (label: string, point: Point, height: number, angle: number) => {
|
||||
const radians = (angle * Math.PI) / 180;
|
||||
return textEntity(label, point, {
|
||||
textAlign: 'left',
|
||||
fontSize: height,
|
||||
textDirection: new Vector(Math.cos(radians), Math.sin(radians)),
|
||||
});
|
||||
};
|
||||
|
||||
type TextLine = { point: Point; height: number; angle: number; start: Point | null };
|
||||
/** TEXT 마지막 줄(세션) — 다음 줄 · 시작점 Enter 가 그 아래로 이어 씀 */
|
||||
let lastLine: TextLine | null = null;
|
||||
|
||||
const below = ({ point, height, angle }: TextLine) => {
|
||||
const radians = (angle * Math.PI) / 180;
|
||||
const gap = height * LINE_GAP;
|
||||
return new Point(point.x + Math.sin(radians) * gap, point.y - Math.cos(radians) * gap);
|
||||
};
|
||||
|
||||
/**
|
||||
* TEXT — 「문자의 시작점 지정:」 → 「높이 지정 <h>:」 → 「문자의 회전 각도 지정 <0>:」 → 줄마다 「문자 입력:」.
|
||||
* 줄 Enter = 그 줄 만들고 아래 새 줄 · 빈 줄 Enter = 끝 · 시작점 Enter = 마지막 글 아래(높이 · 각 안 물음).
|
||||
*/
|
||||
export const textToolStateMachine = createSequenceTool({
|
||||
tool: Tool.TEXT,
|
||||
loopFrom: 'line',
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '문자의 시작점을 지정하십시오.' },
|
||||
{ kind: 'text', instructions: '문자를 입력하십시오.' },
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'start',
|
||||
prompt: () => ({
|
||||
text: '문자의 시작점 지정',
|
||||
absent: [opt('JUSTIFY', 'J', '자리맞추기'), opt('STYLE', 'S', '스타일')],
|
||||
...(lastLine ? { default: { kind: 'option', key: 'CONTINUE' } as const } : {}),
|
||||
}),
|
||||
options: { CONTINUE: { mode: 'continue', next: 'line' } },
|
||||
},
|
||||
{
|
||||
kind: 'number',
|
||||
id: 'height',
|
||||
when: (input) => input.mode !== 'continue',
|
||||
prompt: TEXT_HEIGHT,
|
||||
},
|
||||
{
|
||||
kind: 'number',
|
||||
id: 'angle',
|
||||
when: (input) => input.mode !== 'continue',
|
||||
prompt: {
|
||||
text: '문자의 회전 각도 지정',
|
||||
default: { kind: 'value', remember: 'TEXT.ANGLE', initial: 0, format: 'ang' },
|
||||
},
|
||||
},
|
||||
{ kind: 'text', id: 'line', prompt: typing('문자 입력'), options: { DONE: 'end' } },
|
||||
],
|
||||
commit: (input) => {
|
||||
const lines = splitLines(input.text(1));
|
||||
if (!lines.length) return;
|
||||
addEntities(buildTextLines(lines.slice(0, 1), input.point(0)), true);
|
||||
const start = input.has('start') ? input.get<Point>('start') : null;
|
||||
const previous = start ? null : lastLine;
|
||||
const height = previous?.height ?? input.get<number>('height');
|
||||
const angle = previous?.angle ?? input.get<number>('angle');
|
||||
// 같은 명령의 다음 줄 · 시작점 Enter = 마지막 줄 아래
|
||||
const follow = lastLine && (!start || lastLine.start === start) ? lastLine : null;
|
||||
const point = follow ? below(follow) : (start as Point);
|
||||
addEntities([placeText(String(input.get('line')), point, height, angle)], true);
|
||||
lastLine = { point, height, angle, start };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* MTEXT — 「첫 번째 구석 지정:」 → 「반대 구석 지정 또는 [높이(H)/폭(W)]:」 → 「문자 입력:」(줄바꿈 \P).
|
||||
* 첫 줄 = 상자 왼쪽 위 · 줄 감기 없음(줄은 \P 로만).
|
||||
*/
|
||||
export const mtextToolStateMachine = createSequenceTool({
|
||||
tool: Tool.MTEXT,
|
||||
steps: [
|
||||
{ kind: 'point', instructions: '여러 줄 문자의 첫 코너를 지정하십시오.' },
|
||||
{ kind: 'text', instructions: '문자를 입력하십시오 (줄바꿈은 \\P).' },
|
||||
{ kind: 'point', id: 'corner', prompt: { text: '첫 번째 구석 지정' } },
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'opposite',
|
||||
prompt: {
|
||||
text: '반대 구석 지정',
|
||||
options: [opt('HEIGHT', 'H', '높이'), opt('WIDTH', 'W', '폭')],
|
||||
absent: [
|
||||
opt('JUSTIFY', 'J', '자리맞추기'),
|
||||
opt('LINESPACING', 'L', '행 간격'),
|
||||
opt('ROTATION', 'R', '회전'),
|
||||
opt('STYLE', 'S', '스타일'),
|
||||
opt('COLUMNS', 'C', '열'),
|
||||
],
|
||||
},
|
||||
options: { HEIGHT: { mode: 'H', next: 'height' }, WIDTH: { mode: 'W', next: 'width' } },
|
||||
},
|
||||
{
|
||||
kind: 'number',
|
||||
id: 'height',
|
||||
when: (input) => !input.has('opposite'),
|
||||
next: 'opposite',
|
||||
prompt: TEXT_HEIGHT,
|
||||
},
|
||||
{
|
||||
kind: 'point',
|
||||
id: 'width',
|
||||
when: (input) => !input.has('opposite'),
|
||||
prompt: { text: '폭 지정', number: { positive: true } },
|
||||
},
|
||||
{
|
||||
kind: 'text',
|
||||
id: 'text',
|
||||
prompt: typing('문자 입력 (줄바꿈 \\P)'),
|
||||
options: { DONE: 'end' },
|
||||
},
|
||||
],
|
||||
preview: (input) => {
|
||||
const corner = input.get<Point>('corner');
|
||||
if (!corner || input.has('opposite') || input.mode === 'W') return [];
|
||||
const { x, y } = input.cursor;
|
||||
const box = [corner, new Point(x, corner.y), new Point(x, y), new Point(corner.x, y), corner];
|
||||
const outline = polyLineEntity(box);
|
||||
return outline ? [outline] : [];
|
||||
},
|
||||
commit: (input) => {
|
||||
const lines = splitLines(input.text(1));
|
||||
const lines = splitLines(String(input.get('text')));
|
||||
if (!lines.length) return;
|
||||
addEntities(buildTextLines(lines, input.point(0)), true);
|
||||
const corner = input.get<Point>('corner');
|
||||
const opposite = input.has('opposite') ? input.get<Point>('opposite') : corner;
|
||||
const height = input.has('height')
|
||||
? input.get<number>('height')
|
||||
: (recall<number>('TEXTSIZE') ?? textSize());
|
||||
const left = Math.min(corner.x, opposite.x);
|
||||
const top = Math.max(corner.y, opposite.y) - height;
|
||||
const groupId = lines.length > 1 ? crypto.randomUUID() : undefined;
|
||||
const entities = lines.map((line, index) => {
|
||||
const entity = placeText(line, new Point(left, top - height * LINE_GAP * index), height, 0);
|
||||
entity.groupId = groupId;
|
||||
return entity;
|
||||
});
|
||||
addEntities(entities, true);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user