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:
1 parent
55b679932e
commit
372d759eb7
10 files changed
+425
-46
No files matched your search
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user