/** * 고른 범위 → 표 자료(`helpers/table-from-drawing.ts` · M02 83-1) 검증. * 실행: node --experimental-strip-types resources/tester/cad/table-from-drawing.test.ts * 견본 셋 = 표 엔티티 · 선 표(병합 · PolyLine · 끊긴 선) · 글만 + 빈 범위 · 글 없는 선 · 회전된 표. */ import assert from "node:assert/strict"; import { cellValue, tableFromDrawing, type DrawingEntityJson, } from "../../../B07_DesignDetail/openwebcad/src/helpers/table-from-drawing.ts"; const line = (x1: number, y1: number, x2: number, y2: number): DrawingEntityJson => ({ type: "Line", shapeData: { startPoint: { x: x1, y: y1 }, endPoint: { x: x2, y: y2 } }, }); const text = (label: string, x: number, y: number, fontSize = 2.5): DrawingEntityJson => ({ type: "Text", shapeData: { label, basePoint: { x, y }, options: { fontSize, textAlign: "left" } }, }); // 숫자 글 assert.equal(cellValue("1,500"), 1500, "천 단위 쉼표"); assert.equal(cellValue(" -2.75 "), -2.75, "음수 소수"); assert.equal(cellValue("12,34"), "12,34", "쉼표 자리 틀리면 글"); assert.equal(cellValue("D600"), "D600", "글 그대로"); assert.equal(cellValue("1,234,567.5"), 1234567.5); // ① 표 엔티티 — 칸 그대로 · 병합 · 폭 · 높이 const tableEntity: DrawingEntityJson = { type: "Table", shapeData: { origin: { x: 0, y: 100 }, columnWidths: [30, 20, 25], rowHeights: [8, 6], cells: [ [{ text: "구분", rowSpan: 2 }, { text: "수량", colSpan: 2 }, null], [null, { text: "1,500" }, { text: "m2" }], ], }, }; const t1 = tableFromDrawing([tableEntity, text("밖 글", 500, 500)]); assert.ok(t1); assert.equal(t1.kind, "table"); assert.deepEqual(t1.rows, [ ["구분", "수량", ""], ["", 1500, "m2"], ]); assert.deepEqual(t1.merges, [ { row: 0, col: 0, rowSpan: 2, colSpan: 1 }, { row: 0, col: 1, rowSpan: 1, colSpan: 2 }, ]); assert.deepEqual(t1.colWidths, [30, 20, 25]); assert.deepEqual(t1.rowHeights, [8, 6]); // ② 선 표 — 3열 × 3행 · 맨 위 줄 가운데 세로선 빠짐(병합) · 바깥 테두리는 PolyLine · 가로선 한 줄은 끊겨 둘 // x 경계 0 · 40 · 70 · 100 / y 경계 60 · 50 · 40 · 30 const outline: DrawingEntityJson = { type: "PolyLine", shapeData: null, children: [ line(0, 60, 100, 60), line(100, 60, 100, 30), line(100, 30, 0, 30), line(0, 30, 0, 60), ], }; const gridEntities: DrawingEntityJson[] = [ outline, line(0, 50, 55, 50), // 끊긴 가로선 둘(겹침 5) line(50, 50.05, 100, 50.05), // 살짝 어긋남(허용오차 안) line(0, 40, 100, 40), line(40, 60, 40, 30), // 세로선 — 맨 위 줄도 지남 line(70, 50, 70, 30), // 맨 위 줄엔 없음 → (0,1)~(0,2) 병합 text("구분", 2, 53), text("수량", 45, 53), text("흙깎기", 2, 43), text("1,500", 42, 43), text("m3", 72, 43), text("돌쌓기", 2, 33), text("2.5", 42, 33), text("m2", 72, 33), text("(전면)", 2, 31), // 같은 칸 두 줄 → 「돌쌓기 (전면)」 ]; const t2 = tableFromDrawing(gridEntities); assert.ok(t2); assert.equal(t2.kind, "grid"); assert.deepEqual(t2.rows, [ ["구분", "수량", ""], ["흙깎기", 1500, "m3"], ["돌쌓기 (전면)", 2.5, "m2"], ]); assert.deepEqual(t2.merges, [{ row: 0, col: 1, rowSpan: 1, colSpan: 2 }]); assert.deepEqual( t2.colWidths.map((w) => Math.round(w)), [40, 30, 30], ); assert.deepEqual( t2.rowHeights.map((h) => Math.round(h)), [10, 10, 10], ); // 사각형 엔티티 칸 · 짧은 눈금 선은 경계로 안 침 const rectGrid = tableFromDrawing([ { type: "Rectangle", shapeData: { points: [ { x: 0, y: 0 }, { x: 20, y: 0 }, { x: 20, y: 10 }, { x: 0, y: 10 }, ], }, }, { type: "Rectangle", shapeData: { points: [ { x: 20, y: 0 }, { x: 50, y: 0 }, { x: 50, y: 10 }, { x: 20, y: 10 }, ], }, }, line(35, 0, 35, 1), // 눈금 text("가", 2, 4), text("나", 22, 4), ]); assert.ok(rectGrid); assert.equal(rectGrid.kind, "grid"); assert.deepEqual(rectGrid.rows, [["가", "나"]]); assert.deepEqual(rectGrid.colWidths, [20, 30]); // ③ 글만 — 줄(y 묶음) · 칸(x 차례) · 모자란 칸은 빈 칸 const t3 = tableFromDrawing([ text("합계", 0, 10), text("3,000", 40, 10.3), text("비고", 80, 9.8), text("소계", 0, 0), text("1,000", 40, 0), ]); assert.ok(t3); assert.equal(t3.kind, "text"); assert.deepEqual(t3.rows, [ ["합계", 3000, "비고"], ["소계", 1000, ""], ]); assert.deepEqual(t3.merges, []); // 빈 범위 · 글 없는 선 = null assert.equal(tableFromDrawing([]), null, "빈 범위"); assert.equal( tableFromDrawing([line(0, 0, 10, 0), line(0, 5, 10, 5), line(0, 0, 0, 5), line(10, 0, 10, 5)]), null, "글 없는 선", ); // 회전된 표(30°) — 축 선이 없어 글 자리 폴백 const rot = (x: number, y: number): [number, number] => { const a = Math.PI / 6; return [x * Math.cos(a) - y * Math.sin(a), x * Math.sin(a) + y * Math.cos(a)]; }; const rline = (x1: number, y1: number, x2: number, y2: number) => line(...rot(x1, y1), ...rot(x2, y2)); const t4 = tableFromDrawing([ rline(0, 0, 40, 0), rline(0, 10, 40, 10), rline(0, 0, 0, 10), rline(40, 0, 40, 10), rline(20, 0, 20, 10), text("A", ...rot(2, 4)), text("B", ...rot(22, 4)), ]); assert.ok(t4); assert.equal(t4.kind, "text"); assert.equal(t4.rows.flat().filter(Boolean).length, 2, "글 둘 다 살아 있음"); // 격자 밖 글이 반 넘으면 폴백(표가 아닌 범위) const t5 = tableFromDrawing([ line(0, 0, 10, 0), line(0, 10, 10, 10), line(0, 0, 0, 10), line(10, 0, 10, 10), text("안", 2, 4), text("밖1", 50, 50), text("밖2", 80, 50), ]); assert.equal(t5?.kind, "text"); console.log("table-from-drawing: 통과");