Files
Aislo/B07_DesignDetail/openwebcad/src/tools/draw/arc-tool.ts
T
eomsangdonandClaude Opus 5.5 3cebb37ed0 feat(CAD): 337-3a 나-1 그리기 8 — 오토캐드 물음 차례 · 옵션 · 기본값 · 빈 Enter · Esc · 끝난 뒤
- LINE: 「첫 번째 점 지정」 빈 Enter = 마지막 끝점에서 이음 · U = 마지막 토막(되돌림 한 칸) · 둘째 토막부터 C · Enter · Esc = 끝(토막 남음)
- PLINE(새 pline-tool): 호(A) 접선 호 · 길이(L) · 닫기(C · 호 모드 CL) · 명령 취소(U) · 선(L) · Esc · Enter = 그린 데까지 남김
- RECTANG: 치수(D) 길이 · 폭 기억(RECT.L · RECT.W) → 누른 쪽 사분면 · 없는 옵션 안내
- CIRCLE: 3P · 2P · 지름(D) <2r> · 반지름 기억(CIRCLERAD · 처음 <> 없음) · 한 번 뒤 끝
- POLYGON(새 polygon-tool): 변 수 기억 <4> · 3~1024 정수 · 모서리(E) · 내접/외접 기억 · 숫자 = 아래 변 수평
- ARC(새 arc-tool): 세 점 · 중심(C) · 각도(A) · 끝(E) · 반지름(R)
- ELLIPSE(새 ellipse-tool): 축 두 끝 · 중심(C) · 회전(R) · 다른 축 숫자 = 중심에서 반 길이(N10)
- SPLINE: 다음 점 입력 · U · 넷째 점부터 C · Esc 버림 · DONUT 중심 되풀이(loopFrom)
- 시험 draw-acad 22 · keyboard-chain 옛 선 · 폴리선 줄 두 곳 새 동작으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
2026-10-16 00:16:15 +09:00

127 lines
5.4 KiB
TypeScript

/**
* 호(ARC) — 337 설계 1-1 · 오토캐드 차례(순서 엔진 v2).
* 「호의 시작점 지정:」 → 「호의 두 번째 점 지정 또는 [중심(C)/끝(E)]:」 → 「호의 끝점 지정:」(세 점) ·
* C → 「호의 중심점 지정:」 → 「호의 끝점 지정 또는 [각도(A)]:」(A → 「사이각 지정:」) ·
* E → 「호의 끝점 지정:」 → 「호의 중심점 지정 또는 [반지름(R)]:」(R → 「호의 반지름 지정:」) ·
* 중심 · 끝 · 반지름 방식은 시작점에서 반시계(오토캐드) · 사이각 − = 시계 · 반지름 = 작은 호. 하나 그리면 끝.
*/
import { Point } from '@flatten-js/core';
import type { Entity } from '../../entities/Entity';
import { type ArcDefinition, arcThroughThreePoints } from '../../helpers/geometry/shape-points';
import { addEntities } from '../../state';
import { Tool } from '../../tools';
import { arcEntity, lineEntity } from '../factories/entity-factory';
import { type SequenceInput, createSequenceTool } from '../factories/sequence-tool';
import { option, rememberDrawEnd } from './draw-steps';
const angleOf = (center: Point, point: Point) => Math.atan2(point.y - center.y, point.x - center.x);
/** 중심 · 시작점 · 끝각 — 반시계(사이각 − 면 시계) */
const arcAround = (center: Point, start: Point, endAngle: number, ccw = true): ArcDefinition => ({
center,
radius: center.distanceTo(start)[0],
startAngle: angleOf(center, start),
endAngle,
counterClockwise: ccw,
});
/** 시작 · 끝 · 반지름 — 반시계 작은 호 · 반지름이 현의 반보다 작으면 null */
function arcByRadius(start: Point, end: Point, radius: number): ArcDefinition | null {
const half = start.distanceTo(end)[0] / 2;
if (half === 0 || radius < half) return null;
const rise = Math.sqrt(radius * radius - half * half);
const ux = (end.x - start.x) / (2 * half);
const uy = (end.y - start.y) / (2 * half);
const center = new Point((start.x + end.x) / 2 - uy * rise, (start.y + end.y) / 2 + ux * rise);
return arcAround(center, start, angleOf(center, end));
}
/** 지금 값으로 호 — 커서(미리보기)가 마지막 빈 값을 채움 */
function arcOf(input: SequenceInput, cursor?: Point): ArcDefinition | null {
const start = input.get<Point>('start');
const at = (id: string) => (input.has(id) ? input.get<Point>(id) : cursor);
if (input.mode === 'C' || input.mode === 'CA') {
const center = input.get<Point>('center');
if (input.mode === 'CA') {
const sweep = input.has('angle') ? input.get<number>('angle') : null;
if (sweep === null) return null;
return arcAround(center, start, angleOf(center, start) + (sweep * Math.PI) / 180, sweep > 0);
}
const end = at('cEnd');
return end ? arcAround(center, start, angleOf(center, end)) : null;
}
if (input.mode === 'E' || input.mode === 'ER') {
const end = input.get<Point>('eEnd');
if (input.mode === 'ER') {
const radius = at('radius');
return radius ? arcByRadius(start, end, end.distanceTo(radius)[0]) : null;
}
const center = at('eCenter');
return center ? arcAround(center, start, angleOf(center, end)) : null;
}
const third = at('third');
return third ? arcThroughThreePoints(start, input.get<Point>('second'), third) : null;
}
const POINT_TEXT = '호의 끝점 지정';
const when =
(...modes: (string | null)[]) =>
(input: SequenceInput) =>
modes.includes(input.mode);
export const arcToolStateMachine = createSequenceTool({
tool: Tool.ARC,
steps: [
{ kind: 'point', id: 'start', prompt: { text: '호의 시작점 지정' } },
{
kind: 'point',
id: 'second',
prompt: {
text: '호의 두 번째 점 지정',
options: [option('CENTER', 'C', '중심'), option('END', 'E', '끝')],
},
options: { CENTER: { mode: 'C', next: 'center' }, END: { mode: 'E', next: 'eEnd' } },
},
{ kind: 'point', id: 'third', prompt: { text: POINT_TEXT }, when: when(null) },
{ kind: 'point', id: 'center', prompt: { text: '호의 중심점 지정' }, when: when('C', 'CA') },
{
kind: 'point',
id: 'cEnd',
prompt: { text: POINT_TEXT, options: [option('ANGLE', 'A', '각도')] },
options: { ANGLE: { mode: 'CA', next: 'angle' } },
when: when('C'),
},
{ kind: 'number', id: 'angle', prompt: { text: '사이각 지정' }, when: when('CA') },
{ kind: 'point', id: 'eEnd', prompt: { text: POINT_TEXT }, when: when('E', 'ER') },
{
kind: 'point',
id: 'eCenter',
prompt: { text: '호의 중심점 지정', options: [option('RADIUS', 'R', '반지름')] },
options: { RADIUS: { mode: 'ER', next: 'radius' } },
when: when('E'),
},
{ kind: 'point', id: 'radius', prompt: { text: '호의 반지름 지정' }, when: when('ER') },
],
preview: (input) => {
const arc = input.values.length > 1 ? arcOf(input, input.cursor) : null;
const shown: Entity[] = arc ? [arcEntity(arc)] : [];
const last = input.points()[input.points().length - 1];
return arc || !last ? shown : [lineEntity(last, input.cursor)];
},
commit: (input) => {
const arc = arcOf(input);
if (!arc) {
// 세 점 한 줄 = 선(오토캐드는 거절 — 버리지 않게 선으로)
if (input.mode === null) {
addEntities([lineEntity(input.get<Point>('start'), input.get<Point>('third'))], true);
}
return;
}
addEntities([arcEntity(arc)], true);
rememberDrawEnd({
x: arc.center.x + arc.radius * Math.cos(arc.endAngle),
y: arc.center.y + arc.radius * Math.sin(arc.endAngle),
});
},
});