- 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
299 lines
9.9 KiB
TypeScript
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([]);
|
|
},
|
|
});
|