Files
Aislo/B07_DesignDetail/openwebcad/src/tools/annotate/leader-table-tools.ts
T
eomsangdonandClaude Opus 5 ea5ce92725 feat(B07): 표를 객체로 만들어 도면의 표가 진짜 표가 되게 한다
표가 DXF의 표로 나가야 한다(사용자 확정). 지금까지 도면의 표는 선과 문자 뭉치라
내보낼 때 고를 수 있는 길이 하나뿐이었다.

- TableEntity: 열별 폭·행별 높이·칸 문자·병합을 한 객체가 들고 있다. 격자선은 담지
  않고 병합 자리에서 선을 끊는 규칙을 표가 스스로 안다(helpers/table-geometry.ts).
  회전·대칭은 지원하지 않는다 — 표는 축에 붙어 있다.
- 명령: TABLE을 표 객체 생성으로 다시 쓰고 TABLEEDIT(칸 문자)·TABLEROW·TABLECOL·
  TABLEMERGE·TABLEUNMERGE를 더했다. EXPLODE는 표를 선과 문자로 흩는다.
- 그립: 좌측 상단으로 표를 옮기고, 열·행 경계로 폭·높이를 바꾼다.
- 백엔드: 유역 정보표와 횡단 수량 산출표를 표 객체로 낸다. 횡단표는 머리행이 폭
  8등분, 본문이 11열 가중치로 격자가 서로 달라 두 경계를 합친 18열로 만들고 병합으로
  원래 칸을 되살렸다 — 손으로 하던 가로선 끊기가 사라졌다.
- 수량 역추출: 값 Text의 결정적 id로 읽던 것을 칸에 실은 key로 읽도록 옮겼다. 이미
  저장된 도면을 위해 옛 방식을 폴백으로 남겼다.

토적도·종단표는 값이 칸이 아니라 측점 위치에 놓이는 성격이라 이관하지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 15:12:19 +09:00

159 lines
6.0 KiB
TypeScript

/** 지시선·표·구름형·스타일 설정 (조사표 5절 지시선·표·표식·주석 축척) */
import { Point } from '@flatten-js/core';
import { toast } from 'react-toastify';
import {
getAnnotationScale,
getDimTextHeight,
setAnnotationScale,
setDimStyle,
} from '../../commands/dim-settings';
import type { Entity } from '../../entities/Entity';
import { EntityName } from '../../entities/Entity';
import type { TextEntity } from '../../entities/TextEntity';
import { revisionCloudPoints } from '../../helpers/geometry/shape-points';
import { addEntities, getEntities, setEntities, setSelectedEntityIds } from '../../state';
import { Tool } from '../../tools';
import { lineEntity, polyLineEntity } from '../factories/entity-factory';
import { createSequenceTool } from '../factories/sequence-tool';
import { annotationText, arrowHead, worldFactor } from './annotation.helpers';
export const mleaderToolStateMachine = createSequenceTool({
tool: Tool.MLEADER,
steps: [
{ kind: 'point', instructions: '지시선 화살표 위치를 지정하십시오.' },
{ kind: 'point', instructions: '지시선 꺾임점을 지정하십시오.' },
{ kind: 'text', instructions: '지시선 문자를 입력하십시오.' },
],
preview: (input) => {
const points = input.points();
if (points.length !== 1) return [];
return [lineEntity(points[0], input.cursor), arrowHead(points[0], input.cursor)];
},
commit: (input) => {
const [tip, knee] = input.points();
const textHeight = getDimTextHeight() / worldFactor();
// 꺾임점에서 문자 쪽으로 짧은 가로선을 하나 더 뽑는다 (AutoCAD 지시선 모양)
const landingLength = textHeight * 2;
const toRight = knee.x >= tip.x;
const landingEnd = new Point(knee.x + (toRight ? landingLength : -landingLength), knee.y);
const label = annotationText(
input.text(2),
new Point(landingEnd.x, landingEnd.y + textHeight * 0.4)
);
const groupId = crypto.randomUUID();
const parts: Entity[] = [
lineEntity(tip, knee),
lineEntity(knee, landingEnd),
arrowHead(tip, knee),
label,
];
for (const part of parts) part.groupId = groupId;
addEntities(parts, true);
},
});
export const mleaderStyleToolStateMachine = createSequenceTool({
tool: Tool.MLEADERSTYLE,
helpers: false,
steps: [
{ kind: 'number', instructions: '지시선 문자 높이를 입력하십시오 <16>.', defaultValue: 16 },
{ kind: 'number', instructions: '화살표 크기를 입력하십시오 <20>.', defaultValue: 20 },
],
commit: (input) => {
setDimStyle(input.number(0), input.number(1), -1);
toast.success(`지시선 스타일: 문자 ${input.number(0)} · 화살표 ${input.number(1)}`);
},
});
/** 지시선 정렬 — 선택한 지시선 문자의 X 위치를 첫 문자에 맞춘다 */
export const mleaderAlignToolStateMachine = createSequenceTool({
tool: Tool.MLEADERALIGN,
helpers: false,
steps: [{ kind: 'selection', instructions: '정렬할 지시선 문자를 선택한 뒤 ENTER.' }],
commit: (input) => {
const texts = input
.entities(0)
.filter((entity) => entity.getType() === EntityName.Text) as TextEntity[];
if (texts.length < 2) {
toast.warn('지시선 문자를 두 개 이상 선택하십시오.');
return;
}
const targetX = texts[0].getBoundingBox().xmin;
for (const text of texts.slice(1)) {
text.move(targetX - text.getBoundingBox().xmin, 0);
}
setEntities([...getEntities()], true);
setSelectedEntityIds([]);
toast.success(`${texts.length}개 지시선 문자를 정렬했습니다.`);
},
});
export const dimStyleToolStateMachine = createSequenceTool({
tool: Tool.DIMSTYLE,
helpers: false,
steps: [
{ kind: 'number', instructions: '치수 문자 높이를 입력하십시오 <16>.', defaultValue: 16 },
{ kind: 'number', instructions: '화살표 크기를 입력하십시오 <20>.', defaultValue: 20 },
{ kind: 'number', instructions: '소수 자릿수를 입력하십시오 <2>.', defaultValue: 2 },
],
commit: (input) => {
setDimStyle(input.number(0), input.number(1), input.number(2));
setEntities([...getEntities()], false);
toast.success('치수 스타일을 바꿨습니다.');
},
});
/** 치수 업데이트 — 스타일을 바꾼 뒤 화면을 다시 그린다 */
export function updateDimensions(): string {
setEntities([...getEntities()], false);
toast.success('치수를 현재 스타일로 갱신했습니다.');
return '치수 업데이트';
}
export const annotationScaleToolStateMachine = createSequenceTool({
tool: Tool.ANNOSCALE,
helpers: false,
steps: [{ kind: 'number', instructions: '주석 축척 배율을 입력하십시오 <1>.', defaultValue: 1 }],
commit: (input) => {
setAnnotationScale(input.number(0));
setEntities([...getEntities()], false);
toast.success(`주석 축척 ${getAnnotationScale()}배`);
},
});
export const revCloudToolStateMachine = createSequenceTool({
tool: Tool.REVCLOUD,
steps: [
{ kind: 'point', instructions: '구름형 경로의 첫 점을 지정하십시오.' },
{ kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 닫습니다.' },
],
repeatLastStep: true,
preview: (input) => {
const preview = polyLineEntity([...input.points(), input.cursor]);
return preview ? [preview] : [];
},
commit: (input) => {
const points = input.points();
if (points.length < 2) return;
const closed = [...points, points[0].clone()];
const box = closed.reduce(
(size, point) => ({
minX: Math.min(size.minX, point.x),
minY: Math.min(size.minY, point.y),
maxX: Math.max(size.maxX, point.x),
maxY: Math.max(size.maxY, point.y),
}),
{
minX: Number.POSITIVE_INFINITY,
minY: Number.POSITIVE_INFINITY,
maxX: Number.NEGATIVE_INFINITY,
maxY: Number.NEGATIVE_INFINITY,
}
);
// 스캘럽 크기는 구름 크기에 비례시켜 어떤 축척에서도 비슷하게 보이게 한다
const arcRadius = Math.max(Math.hypot(box.maxX - box.minX, box.maxY - box.minY) / 40, 1e-6);
const cloud = polyLineEntity(revisionCloudPoints(closed, arcRadius));
if (cloud) addEntities([cloud], true);
},
});