feat(abut): ② 확대기초 · 말뚝 카드를 시안대로 작성 (평면 삽도, 버림, 열별 본수)

- 선택: 평면 skew 평행사변형(기본) / 직사각(비활성, 다음 단계), 말뚝 원기둥 D600 / 강관 · PHC(비활성, 규칙 미정)
- 평면 삽도 (src/view/abut-footing-svg.ts): 저판, 벽체 윤곽, 교대축, 말뚝 3열(skew만큼 밀림), 열 위치 · 본수, 폭 치수
- 입력: 폭 4600, 두께 1100, 앞굽 1200 (뒷굽 자동 1000), 버림 여유 · 두께 100 / 100, 말뚝 지름 600 (QA-5), 연단 650, 본수 규칙 간격 2000
- 열 위치 "650 · 2000 · 1300 · 650" 입력 (전면 연단 자동, 합 = 폭 검사), 열별 말뚝 본수 표 − / + (0 = 본수 규칙)
- 자동 값: 뒷굽, 길이(교좌면 길이), 말뚝 길이(①에서 수정), 선단 EL, 본수 규칙 문구. 시안 메모
- 3D: 교대 버림을 저판 아래에 그림 (부재 표시 "버림" 토글 적용), 말뚝 지름 입력 반영, 말뚝 열을 skew만큼 s 방향으로 밀어 배치
- 예전 저장 파일의 앞굽 끝 · 뒷굽 끝은 폭 · 앞굽으로 바꿔 읽음
- 테스트 추가 · 갱신. 전체 661건 통과, 타입 검사 · 빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-04 08:46:58 +09:00
1 parent 55b679932e
commit 372d759eb7
10 files changed
+425 -46

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
/**
* 교대 확대기초 · 말뚝 평면 삽도 (2026-11-04, 시안 ② 확대기초·말뚝 카드). 순수 문자열 함수.
* 생성본 값으로 그린다: 저판(skew 평행사변형, 굵은 선), 벽체 윤곽(실선), 교대축(점선), 말뚝 3열(원), 열 위치, 폭 치수.
* 가로(s)와 세로(t) 축척이 같다. 위가 전면, 아래가 배면이다.
*/
export interface AbutFootingDrawing {
/** 교대축 길이 L (m). */
lengthM: number;
/** s per m of t along the wing (skew): s = s_axis + k·t. */
skewK: number;
/** t (m): 저판 앞굽 끝, 뒷굽 끝, 벽체 전면, 벽체 배면. */
footingFrontM: number;
footingBackM: number;
wallFrontM: number;
wallBackM: number;
/** 말뚝 열: t (m), s (m, 교대축 기준, skew 전), 열 이름 (1열 · 전면 …). */
rows: { t: number; s: number[]; name: string }[];
pileDiameterM: number;
/** 열 위치 치수 (mm, 배면부터): 배면 연단, 간격, 간격, 전면 연단. */
rowDimsMm: [number, number, number, number];
footingWidthMm: number;
}
const INK = '#18212b';
const MUTED = '#55606e';
const label = (x: number, y: number, s: string, anchor = 'start', fill = INK) =>
`<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" font-size="11" fill="${fill}" font-family="JetBrains Mono, monospace">${s}</text>`;
export function abutFootingPlanSvg(d: AbutFootingDrawing): string {
const k = 36; // px per m, both directions
const sMin = Math.min(d.skewK * d.footingFrontM, d.skewK * d.footingBackM) - 0.4;
const sMax = d.lengthM + Math.max(d.skewK * d.footingFrontM, d.skewK * d.footingBackM) + 0.4;
const tTop = d.footingFrontM + 0.4;
const X = (s: number, t: number) => 20 + (s + d.skewK * t - sMin) * k;
const Y = (t: number) => 14 + (tTop - t) * k;
const W = 20 + (sMax - sMin) * k + 170;
const H = Y(d.footingBackM - 0.4) + 34;
const quad = (t0: number, t1: number, style: string) =>
`<polygon points="${[[0, t0], [d.lengthM, t0], [d.lengthM, t1], [0, t1]].map(([s, t]) => `${X(s, t).toFixed(1)},${Y(t).toFixed(1)}`).join(' ')}" ${style}/>`;
const parts: string[] = [];
parts.push(quad(d.footingBackM, d.footingFrontM, `fill="#e6ebf0" stroke="${INK}" stroke-width="2"`));
parts.push(quad(d.wallBackM, d.wallFrontM, `fill="none" stroke="${MUTED}"`));
parts.push(`<line x1="${X(0, 0).toFixed(1)}" y1="${Y(0).toFixed(1)}" x2="${X(d.lengthM, 0).toFixed(1)}" y2="${Y(0).toFixed(1)}" stroke="#c0392b" stroke-dasharray="6 3"/>`);
parts.push(label(X(d.lengthM, 0) + 6, Y(0) + 4, '교대축', 'start', '#c0392b'));
const r = Math.max(2, (d.pileDiameterM / 2) * k);
for (const row of d.rows) {
for (const s of row.s) parts.push(`<circle cx="${X(s, row.t).toFixed(1)}" cy="${Y(row.t).toFixed(1)}" r="${r.toFixed(1)}" fill="#ffffff" stroke="${INK}"/>`);
const tLabel = `${row.t >= 0 ? '+' : '−'}${Math.round(Math.abs(row.t) * 1000)}`;
parts.push(label(X(d.lengthM, row.t) + r + 8, Y(row.t) + 4, `${row.name} · t ${tLabel} · ${row.s.length}본`));
}
const [e1, g1, g2, e2] = d.rowDimsMm;
parts.push(label(20, H - 10, `${d.footingWidthMm} = 뒷굽쪽 ${e1} · ${g1} · ${g2} · ${e2} 전면 · 위 = 전면`, 'start', MUTED));
return [
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="확대기초 평면: skew 평행사변형, 폭 ${d.footingWidthMm}, 말뚝 ${d.rows.map((r2) => r2.s.length).join(' + ')}본">`,
...parts,
'</svg>',
].join('');
}
+26 -4
View File
@@ -12,7 +12,7 @@ import { buildWings, HAUNCH_LEG_M, WING_THICKNESS_M, type WingSolid } from '../c
import type { TerrainGrid } from '../core/ground-model';
import { terrainGeometry } from './terrain';
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'slab'] as const;
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'blinding', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'slab'] as const;
/** 접속슬래브 두께 (m), spec_abut §4.8. */
const SLAB_THICKNESS_M = 0.45;
/** 접속슬래브 길이 (배면 쪽, m), spec_abut §4.8 "7000 (날개벽 축 방향)". */
@@ -118,6 +118,12 @@ function wingDirectionAxis(model: AbutModel): { ws: number; wt: number } {
return { ws: w.e * model.u.e + w.n * model.u.n, wt: w.e * front.e + w.n * front.n };
}
/** Skew of the plan (s per m of t along the wing), the same one the 저판 · 벽체 · 말뚝 열 use. */
export function abutSkewK(model: AbutModel): number {
const d = wingDirectionAxis(model);
return d.ws / d.wt;
}
/**
* Placement of the abutment in the pier scene (2026-11-03). The pier scene maps (E, N, EL) to
* (E − pivot.E, EL, −(N − pivot.N)); the abutment's local (s, t, EL) → (E, N) with s along u and t along the front
@@ -220,6 +226,20 @@ export function buildAbutGroup(
[skewK * members.shape.footingFrontM, members.shape.footingFrontM],
];
add('footing', prism(footProfile, members.shape.footingThicknessM, new THREE.Vector3(0, footingBottom, 0), ex, ez, ey), 0x5a6a78);
// 버림 (2026-11-04): under the 저판, wider by the 여유 on every side, the same skew.
const m = members.shape.blindingMarginM;
const th = members.shape.blindingThicknessM;
if (th > 0) {
const tb = members.shape.footingBackM - m;
const tf = members.shape.footingFrontM + m;
const blindProfile: [number, number][] = [
[skewK * tb - m, tb],
[L + skewK * tb + m, tb],
[L + skewK * tf + m, tf],
[skewK * tf - m, tf],
];
add('blinding', prism(blindProfile, th, new THREE.Vector3(0, footingBottom - th, 0), ex, ez, ey), 0x9aa29a);
}
}
// 받침블럭 (spec §4.2, §6.6): concrete 1200 × 1200 from the 교좌면 to 50 below z, and the 몰탈패드 1000 × 1000 × 50 up to z.
@@ -267,13 +287,15 @@ export function buildAbutGroup(
slabGroup.add(slabMesh);
root.add(slabGroup);
// 말뚝: D 600 (r 0.3) at each row and bar, from the 기초 하단 down to the 선단.
// 말뚝: 원기둥 (지름 입력, 기본 D600) at each row, from the 기초 하단 down to the 선단.
if (members.pileBottomEl !== undefined && footingBottom !== undefined) {
const height = footingBottom - members.pileBottomEl;
for (const row of members.piles) {
for (const s of row.s) {
const geom = new THREE.CylinderGeometry(0.3, 0.3, height, 16);
geom.translate(s, members.pileBottomEl + height / 2, row.t);
// 말뚝 지름은 입력값 (QA-5). Each row moves along s with the skew, like the 저판 (시안: 열마다 skew만큼 밀림).
const r = members.shape.pileDiameterM / 2;
const geom = new THREE.CylinderGeometry(r, r, height, 16);
geom.translate(s + skewK * row.t, members.pileBottomEl + height / 2, row.t);
add('piles', geom, 0x7d8b99);
}
}