137 lines
8.3 KiB
TypeScript
137 lines
8.3 KiB
TypeScript
/**
|
|
* 교대 벽체·흉벽 삽도 (2026-11-03, 시안 ② 벽체·흉벽 카드): 교대축 직각 단면(① 구간)과 정면·배면도. 순수 문자열 함수.
|
|
* 생성본(교대 모델·부재)의 값으로 그린다. 두 그림 모두 가로·세로 축척이 같다(시안 "축척 같음").
|
|
*/
|
|
|
|
export interface AbutWallDrawing {
|
|
/** 교대축 길이 L (m), IP1 → IP3. */
|
|
lengthM: number;
|
|
/** 교좌면 계단 구간 (s, m는 IP1에서), 구간마다 교좌면 EL. */
|
|
segments: { sFrom: number; sTo: number; seatEl: number }[];
|
|
/** 받침 (s, m), 받침 번호, 패드 상면 z (m), 교좌면 EL (m). */
|
|
bearings: { no: number; s: number; z: number; seatEl: number }[];
|
|
footingTopEl: number;
|
|
footingBottomEl: number;
|
|
/** 흉벽 상단 EL at s = 0 (IP1 끝) and s = L (IP3 끝). */
|
|
parapetTopStartEl: number;
|
|
parapetTopEndEl: number;
|
|
/** t (m, 전면 +): 벽체 전면, 흉벽 배면, 벽체 배면, 저판 앞굽 끝·뒷굽 끝. */
|
|
wallFrontM: number;
|
|
parapetBackM: number;
|
|
wallBackM: number;
|
|
footingFrontM: number;
|
|
footingBackM: number;
|
|
bracketHeightM: number;
|
|
shrinkJointCount: number;
|
|
/** 시안 치수 줄 "브라켓 폭 + 흉벽 두께 + 교좌면 폭 = 벽체 두께" (mm). */
|
|
bracketWidthMm: number;
|
|
parapetThicknessMm: number;
|
|
seatWidthMm: number;
|
|
}
|
|
|
|
const INK = '#18212b';
|
|
const MUTED = '#55606e';
|
|
const CONC = '#d9c9a8';
|
|
const CONC_DARK = '#c7b48d';
|
|
const FOOT = '#b9c3ce';
|
|
const t3 = (v: number) => v.toFixed(3);
|
|
/** EL 표기 (2026-11-04): "EL.41.995". */
|
|
const el = (v: number) => `EL.${v.toFixed(3)}`;
|
|
const label = (x: number, y: number, s: string, anchor = 'middle', 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>`;
|
|
const poly = (pts: [number, number][], style: string) => `<polygon points="${pts.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(' ')}" ${style}/>`;
|
|
|
|
/** 흉벽 상단 EL at s (linear between the two ends). */
|
|
function parapetTopAt(d: AbutWallDrawing, s: number): number {
|
|
return d.parapetTopStartEl + ((d.parapetTopEndEl - d.parapetTopStartEl) * s) / d.lengthM;
|
|
}
|
|
|
|
/**
|
|
* 교대축 직각 단면 (① 구간 = 받침 1번이 놓인 구간): 저판, 벽체(기초 상면 ~ 교좌면), 흉벽 배면 단차부(브라켓 상면까지),
|
|
* 흉벽(흉벽 상단까지), 받침 블럭. 왼쪽이 배면, 오른쪽이 전면.
|
|
*/
|
|
export function abutWallSectionSvg(d: AbutWallDrawing): string {
|
|
const b1 = d.bearings.find((b) => b.no === 1) ?? d.bearings[0];
|
|
const seg = d.segments.find((g) => b1.s >= g.sFrom && b1.s <= g.sTo) ?? d.segments[0];
|
|
const sAt = b1.s;
|
|
const top = parapetTopAt(d, sAt);
|
|
const bracket = top - d.bracketHeightM;
|
|
const k = 46; // px per m, both directions
|
|
const tMin = d.footingBackM - 0.6;
|
|
const elMax = top + 0.7;
|
|
const X = (t: number) => (t - tMin) * k;
|
|
const Y = (el: number) => (elMax - el) * k + 10;
|
|
const W = (d.footingFrontM + 1.4 - tMin) * k;
|
|
const H = Y(d.footingBottomEl) + 40;
|
|
const rect = (t0: number, t1: number, e0: number, e1: number, style: string) =>
|
|
poly([[X(t0), Y(e0)], [X(t1), Y(e0)], [X(t1), Y(e1)], [X(t0), Y(e1)]], style);
|
|
const halfBlock = 0.6;
|
|
const blockT = (d.wallFrontM + 0) / 2; // drawn at the middle of the 교좌면 width
|
|
return [
|
|
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="교대축 직각 단면 (① 구간): 흉벽 상단 ${t3(top)}, 브라켓 상면 ${t3(bracket)}, 교좌면 ${t3(seg.seatEl)}, 저판 상면 ${t3(d.footingTopEl)}, 기초 하단 ${t3(d.footingBottomEl)}">`,
|
|
rect(d.footingBackM, d.footingFrontM, d.footingBottomEl, d.footingTopEl, `fill="${FOOT}" stroke="${MUTED}"`),
|
|
rect(d.wallBackM, d.wallFrontM, d.footingTopEl, seg.seatEl, `fill="${CONC}" stroke="${MUTED}"`),
|
|
rect(d.wallBackM, d.parapetBackM, seg.seatEl, bracket, `fill="${CONC_DARK}" stroke="${MUTED}"`),
|
|
rect(d.parapetBackM, 0, seg.seatEl, top, `fill="${CONC_DARK}" stroke="${MUTED}"`),
|
|
rect(blockT - halfBlock, blockT + halfBlock, seg.seatEl, b1.z, `fill="#9aa7b5" stroke="${INK}"`),
|
|
`<line x1="${X(0).toFixed(1)}" y1="4" x2="${X(0).toFixed(1)}" y2="${(H - 30).toFixed(1)}" stroke="#c0392b" stroke-dasharray="6 3"/>`,
|
|
label(X(0), 14, '교대축', 'middle', '#c0392b'),
|
|
label((X(d.wallBackM) + X(d.wallFrontM)) / 2, Y(top) - 18, `${d.bracketWidthMm} + ${d.parapetThicknessMm} + ${d.seatWidthMm} = ${d.bracketWidthMm + d.parapetThicknessMm + d.seatWidthMm}`),
|
|
label(X(0) + 6, Y(top) - 4, `흉벽 상단 ${el(top)}`, 'start'),
|
|
label(X(d.wallBackM) - 6, Y(bracket) + 4, `브라켓 상면 ${el(bracket)}`, 'end'),
|
|
label(X(d.wallFrontM) + 6, Y(seg.seatEl) + 4, `교좌면 ${el(seg.seatEl)}`, 'start'),
|
|
label(X(d.wallFrontM) + 6, Y((seg.seatEl + d.footingTopEl) / 2) + 4, `벽체 ${(seg.seatEl - d.footingTopEl).toFixed(3)}`, 'start'),
|
|
label(X(d.footingFrontM) + 6, Y(d.footingTopEl) + 4, `상면 ${el(d.footingTopEl)}`, 'start'),
|
|
label(X(d.footingFrontM) + 6, Y(d.footingBottomEl) + 4, `기초 하단 ${el(d.footingBottomEl)}`, 'start'),
|
|
label(X(d.footingBackM), H - 8, '← 배면', 'start', MUTED),
|
|
label(X(d.footingFrontM), H - 8, '전면 →', 'end', MUTED),
|
|
'</svg>',
|
|
].join('');
|
|
}
|
|
|
|
/**
|
|
* 정면·배면도: 교좌면 계단(구간별 교좌면 EL), 흉벽 상단 경사, 받침 블럭, 수축줄눈, 저판. 왼쪽 IP3(① 쪽), 오른쪽 IP1.
|
|
*/
|
|
export function abutWallElevationSvg(d: AbutWallDrawing): string {
|
|
const k = 46;
|
|
const top = Math.max(d.parapetTopStartEl, d.parapetTopEndEl);
|
|
const elMax = top + 0.6;
|
|
const pad = 40;
|
|
const X = (s: number) => pad + (d.lengthM - s) * k; // IP3 (s = L) on the left
|
|
const Y = (el: number) => (elMax - el) * k + 10;
|
|
const W = d.lengthM * k + 2 * pad;
|
|
const H = Y(d.footingBottomEl) + 40;
|
|
const parts: string[] = [];
|
|
parts.push(poly([[X(d.lengthM), Y(d.footingTopEl)], [X(0), Y(d.footingTopEl)], [X(0), Y(d.footingBottomEl)], [X(d.lengthM), Y(d.footingBottomEl)]], `fill="${FOOT}" stroke="${MUTED}"`));
|
|
// 흉벽 (behind) from the 교좌면 steps up to the sloped top line.
|
|
parts.push(poly([[X(d.lengthM), Y(d.footingTopEl)], [X(0), Y(d.footingTopEl)], [X(0), Y(d.parapetTopStartEl)], [X(d.lengthM), Y(d.parapetTopEndEl)]], `fill="${CONC_DARK}" stroke="${MUTED}"`));
|
|
// 벽체 front: one block per 교좌면 step.
|
|
for (const g of d.segments) {
|
|
parts.push(poly([[X(g.sTo), Y(d.footingTopEl)], [X(g.sFrom), Y(d.footingTopEl)], [X(g.sFrom), Y(g.seatEl)], [X(g.sTo), Y(g.seatEl)]], `fill="${CONC}" stroke="${MUTED}"`));
|
|
}
|
|
for (const b of d.bearings) {
|
|
parts.push(poly([[X(b.s + 0.6), Y(b.seatEl)], [X(b.s - 0.6), Y(b.seatEl)], [X(b.s - 0.6), Y(b.z)], [X(b.s + 0.6), Y(b.z)]], `fill="#9aa7b5" stroke="${INK}"`));
|
|
parts.push(label(X(b.s), Y(b.z) - 6, ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧'][b.no - 1] ?? String(b.no)));
|
|
}
|
|
for (let j = 1; j <= d.shrinkJointCount; j++) {
|
|
const s = (d.lengthM * j) / (d.shrinkJointCount + 1);
|
|
parts.push(`<line x1="${X(s).toFixed(1)}" y1="${Y(parapetTopAt(d, s)).toFixed(1)}" x2="${X(s).toFixed(1)}" y2="${Y(d.footingTopEl).toFixed(1)}" stroke="${INK}" stroke-dasharray="5 3"/>`);
|
|
parts.push(label(X(s) + 4, Y(d.footingTopEl) - 8, `수축줄눈 ${j}`, 'start'));
|
|
}
|
|
const first = d.segments.find((g) => d.lengthM >= g.sFrom && d.lengthM <= g.sTo) ?? d.segments[d.segments.length - 1];
|
|
const last = d.segments.find((g) => g.sFrom <= 0 && g.sTo >= 0) ?? d.segments[0];
|
|
parts.push(label(X(d.lengthM), Y(d.parapetTopEndEl) - 6, el(d.parapetTopEndEl), 'start'));
|
|
parts.push(label(X(0), Y(d.parapetTopStartEl) - 6, el(d.parapetTopStartEl), 'end'));
|
|
parts.push(label(X(d.lengthM) - 4, Y(first.seatEl) + 4, el(first.seatEl), 'end'));
|
|
parts.push(label(X(0) + 4, Y(last.seatEl) + 4, el(last.seatEl), 'start'));
|
|
parts.push(label(X(d.lengthM) - 4, Y(d.footingBottomEl) + 4, el(d.footingBottomEl), 'end'));
|
|
parts.push(label(X(d.lengthM), H - 8, 'IP3', 'middle', MUTED));
|
|
parts.push(label(X(0), H - 8, 'IP1', 'middle', MUTED));
|
|
parts.push(label((X(0) + X(d.lengthM)) / 2, H - 8, `${d.lengthM.toFixed(3)} m`));
|
|
return [
|
|
`<svg viewBox="0 0 ${W.toFixed(0)} ${H.toFixed(0)}" width="100%" role="img" aria-label="정면·배면도: 흉벽 상단 ${t3(d.parapetTopEndEl)} (IP3) → ${t3(d.parapetTopStartEl)} (IP1), 교좌면 계단 ${d.segments.length}구간, 수축줄눈 ${d.shrinkJointCount}">`,
|
|
...parts,
|
|
'</svg>',
|
|
].join('');
|
|
}
|