Files
Aislo/B07_DesignDetail/openwebcad/src/tools/annotate/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

193 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 표 명령 — 표 객체 하나를 만들고 칸을 고친다 (조사표 5절 표 · 12절 테이블 셀).
* 셀을 고르는 방법은 이 CAD의 다른 명령과 같다: 점을 찍으면 그 자리의 칸을 집는다.
*/
import type { Point } from '@flatten-js/core';
import { toast } from 'react-toastify';
import { getTableColumnWidth, getTableRowHeight, setTableStyle } from '../../commands/dim-settings';
import type { Entity } from '../../entities/Entity';
import { TableEntity } from '../../entities/TableEntity';
import { cellAt } from '../../helpers/table-geometry';
import {
addEntities,
getActiveLayerId,
getActiveTextStyle,
getEntities,
setEntities,
} from '../../state';
import { Tool } from '../../tools';
import { createSequenceTool } from '../factories/sequence-tool';
/** 점이 놓인 표와 그 칸. 표를 못 찾으면 null */
function findTableCell(point: Point): { table: TableEntity; row: number; column: number } | null {
for (const entity of getEntities()) {
if (!(entity instanceof TableEntity)) continue;
const hit = cellAt(
entity.getOrigin(),
entity.getColumnWidths(),
entity.getRowHeights(),
entity.getCells(),
point
);
if (hit) return { table: entity, row: hit.row, column: hit.column };
}
return null;
}
/** 표를 고친 뒤 화면·실행취소에 반영한다 */
function commitTableChange(table: TableEntity): void {
setEntities(
getEntities().map((entity: Entity) => (entity.id === table.id ? table : entity)),
true
);
}
function withTableAt(
point: Point,
apply: (hit: NonNullable<ReturnType<typeof findTableCell>>) => string | null
): void {
const hit = findTableCell(point);
if (!hit) {
toast.info('표 안의 칸을 지정하십시오.');
return;
}
const message = apply(hit);
commitTableChange(hit.table);
if (message) toast.success(message);
}
export const tableToolStateMachine = createSequenceTool({
tool: Tool.TABLE,
steps: [
{ kind: 'number', instructions: '열 수를 입력하십시오 <3>.', defaultValue: 3 },
{ kind: 'number', instructions: '행 수를 입력하십시오 <3>.', defaultValue: 3 },
{ kind: 'point', instructions: '표의 좌측 상단 삽입점을 지정하십시오.' },
],
commit: (input) => {
const columns = Math.max(1, Math.round(input.number(0)));
const rows = Math.max(1, Math.round(input.number(1)));
const origin = input.point(2);
const table = new TableEntity(
getActiveLayerId(),
origin,
Array.from({ length: columns }, () => getTableColumnWidth()),
Array.from({ length: rows }, () => getTableRowHeight()),
undefined,
{ fontSize: getActiveTextStyle().fontSize, textColor: getActiveTextStyle().textColor }
);
addEntities([table], true);
toast.info('표를 만들었습니다. 칸 내용은 TABLEEDIT 명령으로 채우십시오.');
},
});
export const tableStyleToolStateMachine = createSequenceTool({
tool: Tool.TABLESTYLE,
helpers: false,
steps: [
{ kind: 'number', instructions: '열 너비를 입력하십시오 <40>.', defaultValue: 40 },
{ kind: 'number', instructions: '행 높이를 입력하십시오 <10>.', defaultValue: 10 },
],
commit: (input) => {
setTableStyle(input.number(0), input.number(1));
toast.success(`표 스타일: 열 ${input.number(0)} · 행 ${input.number(1)}`);
},
});
export const tableEditToolStateMachine = createSequenceTool({
tool: Tool.TABLEEDIT,
steps: [
{ kind: 'point', instructions: '내용을 채울 칸을 지정하십시오.' },
{ kind: 'text', instructions: '칸에 넣을 문자를 입력하십시오.' },
],
commit: (input) => {
const text = input.text(1);
withTableAt(input.point(0), (hit) => {
hit.table.setCell(hit.row, hit.column, { text });
return `칸에 '${text}'를 넣었습니다.`;
});
},
});
export const tableRowToolStateMachine = createSequenceTool({
tool: Tool.TABLEROW,
steps: [
{ kind: 'point', instructions: '기준이 될 칸을 지정하십시오.' },
{
kind: 'number',
instructions: '위에 넣으려면 1, 아래에 넣으려면 2, 지우려면 0 <1>.',
defaultValue: 1,
},
],
commit: (input) => {
const mode = Math.round(input.number(1));
withTableAt(input.point(0), (hit) => {
if (mode === 0) {
hit.table.deleteRow(hit.row);
return '행을 지웠습니다.';
}
hit.table.insertRow(mode === 2 ? hit.row + 1 : hit.row);
return '행을 넣었습니다.';
});
},
});
export const tableColumnToolStateMachine = createSequenceTool({
tool: Tool.TABLECOL,
steps: [
{ kind: 'point', instructions: '기준이 될 칸을 지정하십시오.' },
{
kind: 'number',
instructions: '왼쪽에 넣으려면 1, 오른쪽에 넣으려면 2, 지우려면 0 <1>.',
defaultValue: 1,
},
],
commit: (input) => {
const mode = Math.round(input.number(1));
withTableAt(input.point(0), (hit) => {
if (mode === 0) {
hit.table.deleteColumn(hit.column);
return '열을 지웠습니다.';
}
hit.table.insertColumn(mode === 2 ? hit.column + 1 : hit.column);
return '열을 넣었습니다.';
});
},
});
export const tableMergeToolStateMachine = createSequenceTool({
tool: Tool.TABLEMERGE,
steps: [
{ kind: 'point', instructions: '병합할 범위의 왼쪽 위 칸을 지정하십시오.' },
{ kind: 'point', instructions: '병합할 범위의 오른쪽 아래 칸을 지정하십시오.' },
],
commit: (input) => {
const end = findTableCell(input.point(1));
withTableAt(input.point(0), (hit) => {
if (!end || end.table.id !== hit.table.id) {
toast.info('같은 표 안에서 두 칸을 지정하십시오.');
return null;
}
const rowSpan = Math.abs(end.row - hit.row) + 1;
const colSpan = Math.abs(end.column - hit.column) + 1;
hit.table.mergeCells(
Math.min(hit.row, end.row),
Math.min(hit.column, end.column),
colSpan,
rowSpan
);
return `칸 ${rowSpan}×${colSpan}을 합쳤습니다.`;
});
},
});
export const tableUnmergeToolStateMachine = createSequenceTool({
tool: Tool.TABLEUNMERGE,
steps: [{ kind: 'point', instructions: '병합을 풀 칸을 지정하십시오.' }],
commit: (input) => {
withTableAt(input.point(0), (hit) => {
hit.table.unmergeCells(hit.row, hit.column);
return '병합을 풀었습니다.';
});
},
});