feat(abut): 교대 지반·지층 카드 — 교각과 같은 위치 · 수위 · 지층 행 입력, 교대 단면 삽도, 검사
- 위치 (육상 · 하상 · 해상)와 위치별 수위 입력, 지층 행 표 (교각과 공용 strataRowsEditor), 지반 모델 지층 불러오기 - 검사: 교각과 같은 G01–G14, 말뚝 근입 G04, 지반고 < 저판 하면이면 정보 AI01 - 삽도: 지층 · 수위 띠를 교각과 공용으로 빼고, 교대축 직각 단면(저판 · 벽체 · 흉벽 · 말뚝 열)을 EL. 표기로 그림 - 자동 값: 지반고 (엑셀 입력), 연암층 상면 EL, 지층 수. 프로젝트 파일 저장 · 검증 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9835fb42b3
commit
59c2607418
12 files changed
+527
-135
No files matched your search
+132
-61
@@ -14,73 +14,15 @@ const TOP_PAD = 24;
|
||||
* 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 { yMin } = groundRange(input, [footBottom, ...pileTips]);
|
||||
const yMax = Math.max(input.groundEl, 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>`);
|
||||
}
|
||||
const parts = groundBands(input, y, yMin, WIDTH, (v) => v.toFixed(3));
|
||||
|
||||
// Footing, column stub, and piles.
|
||||
const cx = WIDTH / 2;
|
||||
@@ -100,6 +42,135 @@ export function groundSectionSvg(input: PierInput, model: PierModel): string {
|
||||
return `<svg viewBox="0 0 ${WIDTH} ${H}" width="100%" style="max-width: ${WIDTH}px; height: auto" role="img" aria-label="지반·지층 단면: 지반면, 지층, 지하수위, 기초와 말뚝">${parts.join('')}</svg>`;
|
||||
}
|
||||
|
||||
/** The 지반·지층 values the bands read (a pier input, or an abutment's 지반 values with its 지반고). */
|
||||
type GroundBandInput = Pick<PierInput, 'groundEl' | 'groundPosition' | 'groundWaterOn' | 'groundWaterEl' | 'floodLevelEl' | 'scourDepthM' | 'highWaterEl' | 'lowWaterEl' | 'strataRows'>;
|
||||
|
||||
/** The water line (지하수위 / 설계홍수위 / 고조위), the 저조위 and the 세굴선 of the input, as ELs. */
|
||||
function groundLevels(input: GroundBandInput) {
|
||||
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);
|
||||
const lowLevel = sea && Number.isFinite(input.lowWaterEl) ? input.lowWaterEl : undefined;
|
||||
const scour = river && Number.isFinite(input.scourDepthM) && (input.scourDepthM as number) >= 0 ? (input.scourDepthM as number) : undefined;
|
||||
const scourEl = scour !== undefined ? input.groundEl - scour : undefined;
|
||||
return { river, sea, level, lowLevel, scourEl };
|
||||
}
|
||||
|
||||
/** Lowest EL to draw: 1 m below the deepest of the layers, the structure ELs given, and the water and scour lines. */
|
||||
function groundRange(input: GroundBandInput, structureEls: number[]): { yMin: number } {
|
||||
const rows = input.strataRows ?? [];
|
||||
const g = input.groundEl;
|
||||
const { level, lowLevel, scourEl } = groundLevels(input);
|
||||
const lowestRow = rows.length ? rows[rows.length - 1].bottomEl : g - 1;
|
||||
return { yMin: Math.min(lowestRow, ...structureEls, level ?? g, lowLevel ?? g, scourEl ?? g) - 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 지층 띠, 지반선, 수위선 (지하수위 점선 · 설계홍수위 실선 · 고조위 / 저조위), 세굴선 (2026-11-04: shared by the pier and the
|
||||
* abutment 삽도). `fmt` writes an EL (the pier writes "28.931", the abutment "EL.28.931").
|
||||
*/
|
||||
function groundBands(input: GroundBandInput, y: (el: number) => number, yMin: number, width: number, fmt: (el: number) => string): string[] {
|
||||
const rows = input.strataRows ?? [];
|
||||
const ground = input.groundEl;
|
||||
const { river, sea, level, lowLevel, scourEl } = groundLevels(input);
|
||||
const groundLabel = sea ? '해저면 EL' : '지반고';
|
||||
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">연암층 상면 ${fmt(upper).startsWith('EL') ? '' : 'EL '}${fmt(upper)}</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} ${fmt(ground)}</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} ${fmt(level)}</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 ${fmt(lowLevel)}</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">세굴선 ${fmt(scourEl)}</text>`);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** 교대 지반 삽도 입력 (2026-11-04): the 지반 values and the generated 저판 · 벽체 · 흉벽 · 말뚝 (t in m, 전면 +). */
|
||||
export interface AbutGroundDrawing {
|
||||
ground: GroundBandInput;
|
||||
footingTopEl: number;
|
||||
footingBottomEl: number;
|
||||
seatEl: number;
|
||||
parapetTopEl: number;
|
||||
bracketHeightM: number;
|
||||
wallFrontM: number;
|
||||
wallBackM: number;
|
||||
parapetBackM: number;
|
||||
footingFrontM: number;
|
||||
footingBackM: number;
|
||||
pileRowsT: number[];
|
||||
pileBottomEl?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 교대 지반·지층 단면 (삽도, 2026-11-04): 교대축 직각 단면(저판, 벽체, 브라켓 단차, 흉벽, 말뚝 열)을 지층 · 수위 위에 그린다.
|
||||
* 세로는 EL 한 축척, 가로는 단면 모양을 보이는 모식 축척이다. 왼쪽이 배면.
|
||||
*/
|
||||
export function abutGroundSectionSvg(d: AbutGroundDrawing): string {
|
||||
const g = d.ground;
|
||||
const { yMin } = groundRange(g, [d.footingBottomEl, ...(d.pileBottomEl === undefined ? [] : [d.pileBottomEl])]);
|
||||
const yMax = Math.max(g.groundEl, d.parapetTopEl) + 0.8;
|
||||
const scale = PLOT_H / (yMax - yMin);
|
||||
const y = (el: number) => TOP_PAD + (yMax - el) * scale;
|
||||
const H = TOP_PAD + PLOT_H + 16;
|
||||
const fmt = (v: number) => `EL.${v.toFixed(3)}`;
|
||||
const parts = groundBands(g, y, yMin, WIDTH, fmt);
|
||||
const kx = 150 / (d.footingFrontM - d.footingBackM);
|
||||
const cx = WIDTH / 2;
|
||||
const X = (t: number) => cx + t * kx;
|
||||
const rect = (t0: number, t1: number, e0: number, e1: number, style: string) =>
|
||||
`<rect x="${X(t0)}" y="${y(e1)}" width="${X(t1) - X(t0)}" height="${y(e0) - y(e1)}" ${style}></rect>`;
|
||||
const bracketTop = d.parapetTopEl - d.bracketHeightM;
|
||||
if (d.pileBottomEl !== undefined) {
|
||||
for (const t of d.pileRowsT) parts.push(rect(t - 0.15, t + 0.15, d.pileBottomEl, d.footingBottomEl, 'fill="#7D8A97"'));
|
||||
}
|
||||
parts.push(rect(d.footingBackM, d.footingFrontM, d.footingBottomEl, d.footingTopEl, 'fill="#B7C7D3" stroke="#6E8496"'));
|
||||
parts.push(rect(d.wallBackM, d.wallFrontM, d.footingTopEl, d.seatEl, 'fill="#D8C7AA" stroke="#9C8763"'));
|
||||
parts.push(rect(d.wallBackM, d.parapetBackM, d.seatEl, bracketTop, 'fill="#D8C7AA" stroke="#9C8763"'));
|
||||
parts.push(rect(d.parapetBackM, 0, d.seatEl, d.parapetTopEl, 'fill="#D8C7AA" stroke="#9C8763"'));
|
||||
// Right-aligned at the drawing edge so the longer EL labels are not cut off.
|
||||
parts.push(`<text x="${WIDTH - 4}" y="${y(d.footingBottomEl) + 14}" font-size="12" fill="#18212B" text-anchor="end">기초 하단 ${fmt(d.footingBottomEl)}</text>`);
|
||||
if (d.pileBottomEl !== undefined) parts.push(`<text x="${WIDTH - 4}" y="${y(d.pileBottomEl) + 14}" font-size="12" fill="#18212B" text-anchor="end">말뚝 선단 ${fmt(d.pileBottomEl)}</text>`);
|
||||
parts.push(`<text x="${X(0) + 4}" y="${y(d.parapetTopEl) + 4}" font-size="12" fill="#18212B">흉벽 상단 ${fmt(d.parapetTopEl)}</text>`);
|
||||
parts.push(`<text x="${WIDTH - 4}" y="${H - 4}" font-size="11" fill="#55606E" text-anchor="end">← 배면 · 전면 →</text>`);
|
||||
return `<svg viewBox="0 0 ${WIDTH} ${H}" width="100%" style="max-width: ${WIDTH + 80}px; height: auto" role="img" aria-label="교대 지반·지층 단면: 지반면, 지층, 수위, 저판 · 벽체 · 흉벽 · 말뚝">${parts.join('')}</svg>`;
|
||||
}
|
||||
|
||||
function escapeText(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
Reference in new issue
Block a user