Files
Aislo/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts
T
eomsangdonandClaude Opus 5.5 c15be5116a feat(B07 CAD): 337-4b 다-2 늘이기 · 재기 · 치수 오토캐드 조작 — LENGTHEN · DIVIDE · MEASURE · DIST · DIM 류
- LENGTHEN 새 도구(lengthen-tool.ts) — 측정할 객체 [증분(DE)/퍼센트(P)/전체(T)] <마지막 방식> · 누름 = 현재 길이 알림 · 방식별 값 기억 · 변경할 객체 되풀이 · U = 이 명령 안 바꿈만 · 빈 Enter · Esc 끝 · 선 · 호
- DIVIDE · MEASURE — 기본값 없음 · 정수 2~32767 · 양수 · 블록(B) 없음 안내 · MEASURE 누른 끝부터
- DIST — 오토캐드 꼴 두 줄 기록(거리 · 각도 · 증분) · 다중 점(M) 없음 안내
- DIMLINEAR · DIMALIGNED — 빈 Enter = 객체 선택 · [문자(T)/수평(H)/수직(V)] · 치수 문자 = 기록 · 치수 객체 문자 바꿈 칸(<> = 잰 값)
- DIMCONTINUE · DIMBASELINE — 되풀이 · 명령 취소(U) · 빈 Enter 끝 · DIMRADIUS · DIMDIAMETER [문자(T)] · 그 밖 치수 물음 글
- 시험 measure-acad 11

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

299 lines
9.9 KiB
TypeScript

/**
* 반지름·지름·각도·호길이·세로좌표 치수와 중심 표식 (조사표 5절).
* 오토캐드 물음 차례(337 설계 1-2) — DIMRADIUS · DIMDIAMETER = 「호 또는 원 선택:」 →
* 「치수선의 위치 지정 또는 [문자(T)]:」 · 한 번 뒤 끝 · 그 밖 치수 = 물음 글만.
*/
import { Arc, Circle, Point } from '@flatten-js/core';
import { toast } from 'react-toastify';
import { OPT, type OptionSpec } from '../../commands/prompt';
import { logLine } from '../../commands/run-command';
import { getDimDecimals } from '../../commands/dim-settings';
import type { Entity } from '../../entities/Entity';
import { MeasurementEntity } from '../../entities/MeasurementEntity';
import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sample-entity';
import { addEntities, getEntities, setEntities, setSelectedEntityIds } from '../../state';
import { Tool } from '../../tools';
import { arcEntity, lineEntity } from '../factories/entity-factory';
import { createSequenceTool, type SequenceInput } from '../factories/sequence-tool';
import { annotationText, arrowHead, dimensionText } from './annotation.helpers';
interface CircularShape {
center: Point;
radius: number;
}
/** 원·호에서 중심과 반지름을 꺼낸다 */
function circularOf(entity: Entity): CircularShape | null {
const shape = entity.getShape();
if (shape instanceof Circle) return { center: shape.center, radius: Number(shape.r) };
if (shape instanceof Arc) return { center: shape.center, radius: Number(shape.r) };
return null;
}
/** 잰 값 글(「R10.00」 · 「Ø20.00」) */
function radialLabel(entity: Entity, diameter: boolean): string | null {
const circular = circularOf(entity);
if (!circular) return null;
const value = diameter ? circular.radius * 2 : circular.radius;
return `${diameter ? 'Ø' : 'R'}${value.toFixed(getDimDecimals())}`;
}
/** 바꾼 글(`<>` = 잰 값) · 없으면 잰 값 */
const labelWith = (measured: string, text?: string) =>
text && text !== '<>' ? text.replaceAll('<>', measured) : measured;
function radialDimension(
entity: Entity,
textPoint: Point,
diameter: boolean,
text?: string
): Entity[] | null {
const circular = circularOf(entity);
if (!circular) return null;
const angle = Math.atan2(textPoint.y - circular.center.y, textPoint.x - circular.center.x);
const edge = new Point(
circular.center.x + circular.radius * Math.cos(angle),
circular.center.y + circular.radius * Math.sin(angle)
);
const start = diameter
? new Point(
circular.center.x - circular.radius * Math.cos(angle),
circular.center.y - circular.radius * Math.sin(angle)
)
: circular.center;
return [
lineEntity(start, textPoint),
arrowHead(edge, circular.center),
text && text !== '<>'
? annotationText(labelWith(radialLabel(entity, diameter) as string, text), textPoint)
: dimensionText(
diameter ? circular.radius * 2 : circular.radius,
textPoint,
diameter ? 'Ø' : 'R'
),
];
}
const MTEXT: OptionSpec = { key: 'MTEXT', kw: 'M', en: 'MTEXT', label: '여러 줄 문자' };
const ANGLE: OptionSpec = { key: 'ANGLE', kw: 'A', en: 'ANGLE', label: '각도' };
/** DIMRADIUS · DIMDIAMETER — 원 · 호 → [문자(T)] → 위치 */
function radialTool(tool: Tool, diameter: boolean) {
const measured = (input: SequenceInput) =>
input.has('obj') ? radialLabel(input.get<Entity>('obj'), diameter) : null;
return createSequenceTool({
tool,
steps: [
{ kind: 'entity', id: 'obj', prompt: { text: '호 또는 원 선택' } },
{
kind: 'text',
id: 'txt',
when: (input) => input.mode === 'text',
next: 'place',
defaultValue: '<>',
prompt: (input) => {
const value = measured(input);
return {
text: '치수 문자 입력',
...(value === null ? {} : { default: { kind: 'enter' as const, label: value } }),
};
},
},
{
kind: 'point',
id: 'place',
prompt: { text: '치수선의 위치 지정', options: [OPT.TEXT], absent: [MTEXT, ANGLE] },
options: { TEXT: { mode: 'text', next: 'txt' } },
},
],
commit: (input) => {
const entity = input.get<Entity>('obj');
const text = input.has('txt') ? input.get<string>('txt') : undefined;
const parts = radialDimension(entity, input.get<Point>('place'), diameter, text);
if (!parts) {
toast.warn('원 또는 호를 선택하십시오.');
return;
}
addEntities(parts, true);
logLine(`치수 문자 = ${labelWith(radialLabel(entity, diameter) as string, text)}`);
},
});
}
export const dimRadiusToolStateMachine = radialTool(Tool.DIMRADIUS, false);
export const dimDiameterToolStateMachine = radialTool(Tool.DIMDIAMETER, true);
export const dimAngularToolStateMachine = createSequenceTool({
tool: Tool.DIMANGULAR,
steps: [
{ kind: 'point', prompt: { text: '각도 정점 지정' } },
{ kind: 'point', prompt: { text: '첫 번째 각도 끝점 지정' } },
{ kind: 'point', prompt: { text: '두 번째 각도 끝점 지정' } },
],
commit: (input) => {
const [vertex, first, second] = input.points();
const radius = Math.min(vertex.distanceTo(first)[0], vertex.distanceTo(second)[0]) * 0.6;
if (radius <= 0) return;
const startAngle = Math.atan2(first.y - vertex.y, first.x - vertex.x);
const endAngle = Math.atan2(second.y - vertex.y, second.x - vertex.x);
const sweep = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
const midAngle = startAngle + sweep / 2;
const degrees = (sweep * 180) / Math.PI;
addEntities(
[
lineEntity(vertex, first),
lineEntity(vertex, second),
arcEntity({
center: vertex,
radius,
startAngle,
endAngle,
counterClockwise: true,
}),
annotationText(
`${degrees.toFixed(getDimDecimals())}°`,
new Point(
vertex.x + radius * 1.25 * Math.cos(midAngle),
vertex.y + radius * 1.25 * Math.sin(midAngle)
)
),
],
true
);
},
});
export const dimArcToolStateMachine = createSequenceTool({
tool: Tool.DIMARC,
steps: [
{ kind: 'entity', prompt: { text: '호 또는 폴리선 호 세그먼트 선택' } },
{ kind: 'point', prompt: { text: '호 길이 치수 위치 지정' } },
],
commit: (input) => {
const entity = input.entity(0);
const circular = circularOf(entity);
if (!circular) {
toast.warn('호를 선택하십시오.');
return;
}
const length = polylineLength(sampleEntityPoints(entity));
addEntities([dimensionText(length, input.point(1), '⌒ ')], true);
},
});
export const dimOrdinateToolStateMachine = createSequenceTool({
tool: Tool.DIMORDINATE,
steps: [
{ kind: 'point', prompt: { text: '피쳐 위치 지정' } },
{ kind: 'point', prompt: { text: '지시선 끝점 지정' } },
],
commit: (input) => {
const [feature, leaderEnd] = input.points();
// 지시선이 세로로 길면 X좌표를, 가로로 길면 Y좌표를 기입한다 (AutoCAD와 같다)
const vertical = Math.abs(leaderEnd.y - feature.y) >= Math.abs(leaderEnd.x - feature.x);
const value = vertical ? feature.x : feature.y;
addEntities(
[
lineEntity(feature, leaderEnd),
annotationText(
`${vertical ? 'X' : 'Y'} ${value.toFixed(getDimDecimals())}`,
new Point(leaderEnd.x, leaderEnd.y)
),
],
true
);
},
});
export const centerMarkToolStateMachine = createSequenceTool({
tool: Tool.CENTERMARK,
steps: [{ kind: 'entity', instructions: '중심 표식을 넣을 원 또는 호를 선택하십시오.' }],
commit: (input) => {
const circular = circularOf(input.entity(0));
if (!circular) {
toast.warn('원 또는 호를 선택하십시오.');
return;
}
const size = circular.radius * 0.15;
addEntities(
[
lineEntity(
new Point(circular.center.x - size, circular.center.y),
new Point(circular.center.x + size, circular.center.y)
),
lineEntity(
new Point(circular.center.x, circular.center.y - size),
new Point(circular.center.x, circular.center.y + size)
),
],
true
);
},
});
export const centerLineToolStateMachine = createSequenceTool({
tool: Tool.CENTERLINE,
helpers: false,
steps: [
{ kind: 'entity', instructions: '첫 번째 선을 선택하십시오.' },
{ kind: 'entity', instructions: '두 번째 선을 선택하십시오.' },
],
commit: (input) => {
const first = sampleEntityPoints(input.entity(0));
const second = sampleEntityPoints(input.entity(1));
if (first.length < 2 || second.length < 2) {
toast.warn('두 선을 선택하십시오.');
return;
}
const middle = (a: Point, b: Point) => new Point((a.x + b.x) / 2, (a.y + b.y) / 2);
const line = lineEntity(
middle(first[0], second[0]),
middle(first[first.length - 1], second[second.length - 1])
);
line.lineDash = [12, 4, 2, 4];
addEntities([line], true);
},
});
/** DIMSPACE — 선택한 치수들의 치수선 간격을 고르게 맞춘다 */
export const dimSpaceToolStateMachine = createSequenceTool({
tool: Tool.DIMSPACE,
helpers: false,
steps: [
{ kind: 'selection', instructions: '간격을 맞출 치수를 선택한 뒤 ENTER.' },
{ kind: 'number', instructions: '치수선 간격을 입력하십시오 <10>.', defaultValue: 10 },
],
commit: (input) => {
const dimensions = input
.entities(0)
.filter((entity): entity is MeasurementEntity => entity instanceof MeasurementEntity);
if (dimensions.length < 2) {
toast.warn('치수를 두 개 이상 선택하십시오.');
return;
}
const spacing = input.number(1);
const base = dimensions[0];
const baseStart = base.getStartPoint();
const baseOffset = base.getOffsetPoint();
const direction = new Point(baseOffset.x - baseStart.x, baseOffset.y - baseStart.y);
const length = Math.hypot(direction.x, direction.y) || 1;
dimensions.forEach((dimension, index) => {
if (index === 0) return;
const start = dimension.getStartPoint();
dimension.setOffsetPoint(
new Point(
start.x + (direction.x / length) * (length + spacing * index),
start.y + (direction.y / length) * (length + spacing * index)
)
);
});
setEntities([...getEntities()], true);
setSelectedEntityIds([]);
},
});