- Rules confirmed (승인 2026-10-26): datum MSL; 고조위 HWL and 저조위 LWL as two levels; 세굴 깊이 as for 하상; 파랑과 선박 하중 excluded from this scope. - Checks: G13 levels missing, G10 해저면 not below 고조위, G11 저조위 not below 고조위, G14 pile wall at or below the corrosion allowance reference. - CORROSION_ALLOWANCE_MM = 1 is a PLACEHOLDER reference (not in the RAG documents); the card and G14 mark it as to be confirmed. - Card: the 해상 inputs; the section draws 고조위 (solid), 저조위 (dashed), and labels the ground line 해저면 EL. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
106 lines
6.1 KiB
TypeScript
106 lines
6.1 KiB
TypeScript
import type { PierModel } from '../core/geometry';
|
|
import type { PierInput } from '../core/types';
|
|
import { SUPPORT_STRATUM } from '../core/ground';
|
|
|
|
/** Layer fills from the top down (시안 2026-10-26: 지층 1 light, deeper layers darker). */
|
|
const LAYER_FILLS = ['#EADFC8', '#DCCBA8', '#C9B38D', '#B9A37E', '#A8926F', '#97815F'];
|
|
|
|
const WIDTH = 400;
|
|
const PLOT_H = 300;
|
|
const TOP_PAD = 24;
|
|
|
|
/**
|
|
* 지반·지층 단면 (삽도). Drawn from the inputs and the generated footings and piles, on one vertical EL scale.
|
|
* Horizontal positions are schematic (no dimensions): piles are spread across the footing.
|
|
*/
|
|
export function groundSectionSvg(input: PierInput, model: PierModel): string {
|
|
const rows = input.strataRows ?? [];
|
|
const ground = input.groundEl;
|
|
const footTop = Math.max(...model.footings.map((f) => f.top));
|
|
const footBottom = Math.min(...model.footings.map((f) => f.bottom));
|
|
const pileTips = model.piles.map((p) => p.bottom);
|
|
const lowestRow = rows.length ? rows[rows.length - 1].bottomEl : ground - 1;
|
|
// 하상: the 설계홍수위 (solid) and the 세굴선 replace the groundwater (dashed, 승인 2026-10-26).
|
|
const position = input.groundPosition ?? '육상';
|
|
const river = position === '하상';
|
|
const sea = position === '해상';
|
|
const level = river
|
|
? (Number.isFinite(input.floodLevelEl) ? input.floodLevelEl : undefined)
|
|
: sea
|
|
? (Number.isFinite(input.highWaterEl) ? input.highWaterEl : undefined)
|
|
: (input.groundWaterOn && Number.isFinite(input.groundWaterEl) ? input.groundWaterEl : undefined);
|
|
// 해상: the 저조위 is drawn too (dashed), on the MSL datum.
|
|
const lowLevel = sea && Number.isFinite(input.lowWaterEl) ? input.lowWaterEl : undefined;
|
|
const groundLabel = sea ? '해저면 EL' : '지반고';
|
|
const scour = river && Number.isFinite(input.scourDepthM) && (input.scourDepthM as number) >= 0 ? (input.scourDepthM as number) : undefined;
|
|
const scourEl = scour !== undefined ? ground - scour : undefined;
|
|
|
|
const yMin = Math.min(footBottom, lowestRow, ...pileTips, level ?? ground, lowLevel ?? ground, scourEl ?? ground) - 1;
|
|
const yMax = Math.max(ground, footTop, input.copingTopEl) + 0.5;
|
|
const scale = PLOT_H / (yMax - yMin);
|
|
const y = (el: number) => TOP_PAD + (yMax - el) * scale;
|
|
const H = TOP_PAD + PLOT_H + 16;
|
|
|
|
const parts: string[] = [];
|
|
// Layers: each band runs from the upper boundary (ground for the first) down to its 하단 EL.
|
|
let upper = ground;
|
|
rows.forEach((row, i) => {
|
|
const top = y(upper);
|
|
const bottom = y(row.bottomEl);
|
|
parts.push(`<rect x="0" y="${top}" width="${WIDTH}" height="${bottom - top}" fill="${LAYER_FILLS[i % LAYER_FILLS.length]}"></rect>`);
|
|
parts.push(`<text x="8" y="${(top + bottom) / 2 + 4}" font-size="12" fill="#18212B">${escapeText(row.name)}</text>`);
|
|
if (row.name === SUPPORT_STRATUM) {
|
|
parts.push(`<line x1="0" y1="${top}" x2="${WIDTH}" y2="${top}" stroke="#5B4A2E" stroke-width="2"></line>`);
|
|
// Caption just under the line, so it does not collide with the label of the band above.
|
|
parts.push(`<text x="8" y="${top + 14}" font-size="12" fill="#18212B">연암층 상면 EL ${upper.toFixed(3)}</text>`);
|
|
}
|
|
upper = row.bottomEl;
|
|
});
|
|
if (rows.length === 0 || upper > yMin) {
|
|
parts.push(`<rect x="0" y="${y(upper)}" width="${WIDTH}" height="${y(yMin) - y(upper)}" fill="${LAYER_FILLS[rows.length % LAYER_FILLS.length]}"></rect>`);
|
|
}
|
|
|
|
// Ground line and label.
|
|
parts.push(`<line x1="0" y1="${y(ground)}" x2="${WIDTH}" y2="${y(ground)}" stroke="#8A6D3B" stroke-width="1.5"></line>`);
|
|
parts.push(`<text x="8" y="${y(ground) - 6}" font-size="12" fill="#18212B">${groundLabel} ${ground.toFixed(3)}</text>`);
|
|
|
|
// Groundwater (육상, when on) as a dashed line; the 설계홍수위 (하상) as a solid line.
|
|
if (level !== undefined) {
|
|
const dash = river ? '' : ' stroke-dasharray="6 4"';
|
|
const label = river ? '설계홍수위 DHWL' : sea ? '고조위 HWL' : '지하수위';
|
|
parts.push(`<line x1="0" y1="${y(level)}" x2="${WIDTH}" y2="${y(level)}" stroke="#2F6DB5" stroke-width="${river ? 2 : 1}"${dash}></line>`);
|
|
parts.push(`<text x="8" y="${y(level) - 4}" font-size="12" fill="#123E70">${label} ${level.toFixed(3)}</text>`);
|
|
}
|
|
// 해상: 저조위 (dashed, MSL datum).
|
|
if (lowLevel !== undefined) {
|
|
parts.push(`<line x1="0" y1="${y(lowLevel)}" x2="${WIDTH}" y2="${y(lowLevel)}" stroke="#2F6DB5" stroke-dasharray="6 4"></line>`);
|
|
parts.push(`<text x="8" y="${y(lowLevel) + 12}" font-size="12" fill="#123E70">저조위 LWL ${lowLevel.toFixed(3)}</text>`);
|
|
}
|
|
// 세굴선 (하상·해상): the bed after scour.
|
|
if (scourEl !== undefined) {
|
|
parts.push(`<line x1="0" y1="${y(scourEl)}" x2="${WIDTH}" y2="${y(scourEl)}" stroke="#8A6D3B" stroke-dasharray="3 3"></line>`);
|
|
parts.push(`<text x="8" y="${y(scourEl) + 12}" font-size="12" fill="#5B4A2E">세굴선 ${scourEl.toFixed(3)}</text>`);
|
|
}
|
|
|
|
// Footing, column stub, and piles.
|
|
const cx = WIDTH / 2;
|
|
const colW = 30;
|
|
parts.push(`<rect x="${cx - colW / 2}" y="${y(yMax)}" width="${colW}" height="${y(footTop) - y(yMax)}" fill="#D8C7AA" stroke="#9C8763"></rect>`);
|
|
const footW = 140;
|
|
parts.push(`<rect x="${cx - footW / 2}" y="${y(footTop)}" width="${footW}" height="${y(footBottom) - y(footTop)}" fill="#B7C7D3" stroke="#6E8496"></rect>`);
|
|
const sValues = [...new Set(model.piles.map((p) => p.s))].sort((a, b) => a - b);
|
|
const spread = sValues.length > 1 ? (footW - 20) / (sValues.length - 1) : 0;
|
|
sValues.forEach((s) => {
|
|
const tips = model.piles.filter((p) => p.s === s);
|
|
const x = sValues.length > 1 ? cx - footW / 2 + 10 + sValues.indexOf(s) * spread : cx;
|
|
const tip = Math.min(...tips.map((p) => p.bottom));
|
|
parts.push(`<rect x="${x - 3}" y="${y(footBottom)}" width="6" height="${y(tip) - y(footBottom)}" fill="#7D8A97"></rect>`);
|
|
});
|
|
|
|
return `<svg viewBox="0 0 ${WIDTH} ${H}" width="100%" style="max-width: ${WIDTH}px; height: auto" role="img" aria-label="지반·지층 단면: 지반면, 지층, 지하수위, 기초와 말뚝">${parts.join('')}</svg>`;
|
|
}
|
|
|
|
function escapeText(s: string): string {
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|