feat(abut): 날개벽 측면 삽도에 길이 · 끝단 높이 · 뒷굽 끝 치수선 추가

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-04 09:49:30 +09:00
1 parent 41b3165543
commit fc0370883f
2 files changed
+22 -7

No files matched your search

+21 -7
View File
@@ -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) =>
`<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${INK}" stroke-width="0.8"/>`;
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(`<line x1="${X(d.heelDistanceM).toFixed(1)}" y1="${Y(footingBottom).toFixed(1)}" x2="${X(d.heelDistanceM).toFixed(1)}" y2="${(Y(footingBottom) + 10).toFixed(1)}" stroke="${INK}"/>`);
}
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 [
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="${d.name} 측면: 길이 ${Math.round(d.lengthM * 1000)}, 흉벽 상단 ${el(d.parapetTopEl)}, 브라켓 상면 ${el(bracketTop)}, 뒷굽 끝 ${d.heelDistanceM.toFixed(3)} m">`,
...parts,
+1
View File
@@ -65,6 +65,7 @@ describe('날개벽 삽도 (시안 ② 날개벽 카드)', () => {
expect(svg).toMatch(/흉벽 상단 EL\.4[67]\.\d{3}/);
expect(svg).toMatch(/브라켓 상면 EL\.4[56]\.\d{3}/);
expect(svg.match(/<polygon /g)).toHaveLength(5); // wing, 저판, 벽체, 브라켓 단차, 흉벽
expect(svg.match(/stroke-width="0.8"/g)).toHaveLength(15); // 치수선 3개 (6500, 1500, 뒷굽 끝) × (보조선 2 + 치수선 1 + 틱 2)
});
it('모서리: haunch, bracket, slab chamfer and gaps from the inputs', () => {