// 수확기 rect 변환(B07_DesignDetail_UI_Cad_Structures.rectPoints) 자체검증 — 로직 복제. // 돌쌓기 돌·돌망태 칸은 rect + rotate(벽기울기) 로 그린다. 회전을 빠뜨리면 돌이 벽을 // 삐져나가 클립에 전부 잘려 나간다(2026-09-03 실측: rect 49개가 CAD 에 안 실렸다). // 실행: node tmp/tests/test_cad_rect_harvest.mjs import assert from "node:assert/strict"; const flip = (x, y) => [x, -y]; function rectPoints(attrs, transform) { const { x, y, width, height } = attrs; const corners = [ [x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y], ]; const rotate = /rotate\(\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)\s*\)/.exec(transform ?? ""); if (!rotate) return corners.map(([cx, cy]) => flip(cx, cy)); const angle = (Number(rotate[1]) * Math.PI) / 180; const [ox, oy] = [Number(rotate[2]), Number(rotate[3])]; const cos = Math.cos(angle); const sin = Math.sin(angle); return corners.map(([cx, cy]) => { const dx = cx - ox; const dy = cy - oy; return flip(ox + dx * cos - dy * sin, oy + dx * sin + dy * cos); }); } const near = (a, b) => Math.abs(a - b) < 1e-9; // ① 회전이 없으면 네 모서리를 y만 뒤집어 닫힌 점열로 낸다. { const points = rectPoints({ x: 10, y: 20, width: 4, height: 6 }, null); assert.equal(points.length, 5); assert.deepEqual(points[0], [10, -20]); assert.deepEqual(points[2], [14, -26]); assert.deepEqual(points[0], points[4], "닫힌 점열이어야 한다"); } // ② rotate(90 cx cy) — 중심 기준 90° 회전. 폭·높이가 맞바뀐다. { const points = rectPoints({ x: -1, y: -2, width: 2, height: 4 }, "rotate(90 0 0)"); const xs = points.map((p) => p[0]); const ys = points.map((p) => p[1]); assert.ok(near(Math.max(...xs) - Math.min(...xs), 4), "가로가 원래 세로가 된다"); assert.ok(near(Math.max(...ys) - Math.min(...ys), 2), "세로가 원래 가로가 된다"); } // ③ 회전 중심은 제자리에 남는다 — 벽 기울기만큼 기울여도 돌 중심이 안 움직인다. { const attrs = { x: 8, y: 12, width: 4, height: 2 }; const center = [attrs.x + attrs.width / 2, attrs.y + attrs.height / 2]; const points = rectPoints(attrs, `rotate(-16.7 ${center[0]} ${center[1]})`); const xs = points.map((p) => p[0]); const ys = points.map((p) => p[1]); assert.ok(near((Math.min(...xs) + Math.max(...xs)) / 2, center[0])); assert.ok(near((Math.min(...ys) + Math.max(...ys)) / 2, -center[1])); } // ④ 소수·쉼표 구분자도 읽는다 — SVG가 `rotate(-16.7,120.5,88.25)` 로 쓰기도 한다. { const points = rectPoints({ x: 0, y: 0, width: 2, height: 2 }, "rotate(-16.7,1,1)"); assert.ok(!near(points[0][0], 0), "회전이 실제로 적용돼야 한다"); } console.log("OK test_cad_rect_harvest.mjs — 4건");