From fc0370883fecb78c5765d38992766d1278a13ad2 Mon Sep 17 00:00:00 2001 From: ypshim Date: Wed, 4 Nov 2026 09:49:30 +0900 Subject: [PATCH] =?UTF-8?q?feat(abut):=20=EB=82=A0=EA=B0=9C=EB=B2=BD=20?= =?UTF-8?q?=EC=B8=A1=EB=A9=B4=20=EC=82=BD=EB=8F=84=EC=97=90=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20=C2=B7=20=EB=81=9D=EB=8B=A8=20=EB=86=92=EC=9D=B4=20?= =?UTF-8?q?=C2=B7=20=EB=92=B7=EA=B5=BD=20=EB=81=9D=20=EC=B9=98=EC=88=98?= =?UTF-8?q?=EC=84=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Haiku 5.5 --- src/view/abut-wing-svg.ts | 28 +++++++++++++++++++++------- tests/abut-wing-card.test.ts | 1 + 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/view/abut-wing-svg.ts b/src/view/abut-wing-svg.ts index 0e2dfc5..1ff22fd 100644 --- a/src/view/abut-wing-svg.ts +++ b/src/view/abut-wing-svg.ts @@ -87,6 +87,19 @@ const el = (v: number) => `EL.${v.toFixed(3)}`; * 측면 (날개벽 축 방향으로 잘라 본 모습): 벽체 쪽이 오른쪽, 날개벽 끝이 왼쪽. * 벽체 · 흉벽 · 브라켓 단차와 저판을 단면으로 함께 그린다. 가로 위치는 t를 날개벽 거리로 바꾼 값(dM = −t / k, k = 뒷굽 / 뒷굽 거리). */ +/** 치수선 (2026-11-04): 점 a, b에서 치수선 위치 at(가로면 y, 세로면 x)까지 보조선, 치수선, 양 끝 사선 틱, 치수. */ +function dimLine(a: [number, number], b: [number, number], at: number, dir: 'h' | 'v', label: string): string { + const line = (x1: number, y1: number, x2: number, y2: number) => + ``; + const tick = (x: number, y: number) => line(x - 3, y + 3, x + 3, y - 3); + if (dir === 'h') { + const ext = (x: number, y: number) => line(x, y + Math.sign(at - y) * 3, x, at + Math.sign(at - y) * 3); + return [ext(a[0], a[1]), ext(b[0], b[1]), line(a[0], at, b[0], at), tick(a[0], at), tick(b[0], at), text((a[0] + b[0]) / 2, at - 4, label, 'middle')].join(''); + } + const ext = (x: number, y: number) => line(x + Math.sign(at - x) * 3, y, at + Math.sign(at - x) * 3, y); + return [ext(a[0], a[1]), ext(b[0], b[1]), line(at, a[1], at, b[1]), tick(at, a[1]), tick(at, b[1]), text(at - 4, (a[1] + b[1]) / 2 + 4, label, 'end')].join(''); +} + export function abutWingSideSvg(d: AbutWingSideDrawing): string { const k = 40; const kt = Math.abs(d.footingBackM) / d.heelDistanceM; // |t| per m along the wing @@ -100,7 +113,7 @@ export function abutWingSideSvg(d: AbutWingSideDrawing): string { const X = (dM: number) => 40 + (d.lengthM - dM) * k; // wall (dM 0) toward the right const Y = (y: number) => 14 + (elMax - y) * k; const W = X(dmRight) + 110; - const H = Y(elMin) + 30; + const H = Y(elMin) + 34; const rect = (dm0: number, dm1: number, e0: number, e1: number, style: string) => poly([[X(dm0), Y(e0)], [X(dm1), Y(e0)], [X(dm1), Y(e1)], [X(dm0), Y(e1)]], style); const outline: [number, number][] = [ @@ -126,17 +139,18 @@ export function abutWingSideSvg(d: AbutWingSideDrawing): string { const mid = (b.zTopEl + b.zBottomEl) / 2; parts.push(text(X(d.lengthM * 0.55), Y(mid) + 4, b.name, 'middle', MUTED)); } - parts.push(text((X(d.lengthM) + X(0)) / 2, 11, `${Math.round(d.lengthM * 1000)} (교좌면 끝에서, 날개벽 축 방향)`, 'middle')); - if (d.form === 'general') parts.push(text(X(d.lengthM) - 4, (Y(d.topLine[d.topLine.length - 1].el) + Y(d.bottomLine[d.bottomLine.length - 1].el)) / 2 + 4, `${Math.round(d.endHeightM * 1000)}`, 'end')); + // 치수선: 길이 (날개벽 끝 ~ 교좌면 끝), 끝단 높이 (일반형), 뒷굽 끝 (교좌면 끝 ~ 뒷굽 끝). + const endTop = d.topLine[d.topLine.length - 1].el; + const endBottom = d.bottomLine[d.bottomLine.length - 1].el; + parts.push(dimLine([X(d.lengthM), Y(endTop)], [X(0), Y(d.parapetTopEl)], Y(elMax) + 12, 'h', `${Math.round(d.lengthM * 1000)} (교좌면 끝에서, 날개벽 축 방향)`)); + if (d.form === 'general') parts.push(dimLine([X(d.lengthM), Y(endTop)], [X(d.lengthM), Y(endBottom)], X(d.lengthM) - 12, 'v', `${Math.round(d.endHeightM * 1000)}`)); parts.push(text(X(0) + 4, Y(d.parapetTopEl) + 4, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start')); parts.push(text(X(pb) + 4, Y(bracketTop) - 4, `브라켓 상면 ${el(bracketTop)}`, 'start')); parts.push(text(X(wf) + 4, Y(d.seatEl) + 4, `교좌면 ${el(d.seatEl)}`, 'start')); parts.push(text(X(dmRight) + 4, Y(d.footingTopEl) + 4, el(d.footingTopEl), 'start')); parts.push(text(X(dmRight) + 4, Y(footingBottom) + 4, `기초 하단 ${el(footingBottom)}`, 'start')); - if (d.heelDistanceM < d.lengthM) { - parts.push(``); - } - parts.push(text(X(Math.min(d.heelDistanceM, d.lengthM)), Y(footingBottom) + 22, `뒷굽 끝 ${d.heelDistanceM.toFixed(3)} m`, 'middle')); + parts.push(dimLine([X(0), Y(footingBottom)], [X(d.heelDistanceM), Y(footingBottom)], Y(footingBottom) + 18, 'h', '')); + parts.push(text((X(0) + X(d.heelDistanceM)) / 2, Y(footingBottom) + 31, `뒷굽 끝 ${d.heelDistanceM.toFixed(3)} m`, 'middle')); return [ ``, ...parts, diff --git a/tests/abut-wing-card.test.ts b/tests/abut-wing-card.test.ts index 8748aca..d4a760d 100644 --- a/tests/abut-wing-card.test.ts +++ b/tests/abut-wing-card.test.ts @@ -65,6 +65,7 @@ describe('날개벽 삽도 (시안 ② 날개벽 카드)', () => { expect(svg).toMatch(/흉벽 상단 EL\.4[67]\.\d{3}/); expect(svg).toMatch(/브라켓 상면 EL\.4[56]\.\d{3}/); expect(svg.match(/ {