표가 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>
193 lines
6.1 KiB
TypeScript
193 lines
6.1 KiB
TypeScript
/**
|
||
* 표 명령 — 표 객체 하나를 만들고 칸을 고친다 (조사표 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 '병합을 풀었습니다.';
|
||
});
|
||
},
|
||
});
|