/** * 반지름·지름·각도·호길이·세로좌표 치수와 중심 표식 (조사표 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('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('obj'); const text = input.has('txt') ? input.get('txt') : undefined; const parts = radialDimension(entity, input.get('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([]); }, });