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
@@ -7,6 +7,7 @@ import type { WingForm } from './geometry';
|
||||
import { WING_DIMS_DEFAULT, wingDimsIssues, type WingDims } from './wings';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, shapeIssues, type AbutShape } from './shape';
|
||||
import { resolveSlab, slabIssues, type AbutSlab, type SlabForm } from './slab';
|
||||
import type { AbutGround } from './ground';
|
||||
import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table';
|
||||
|
||||
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
|
||||
@@ -149,6 +150,20 @@ export const SLAB_EDIT_FIELDS: { key: 'lengthMm' | 'thicknessMm' | 'parapetGapMm
|
||||
{ key: 'haunchLengthMm', label: '헌치 길이 (mm)', exposedOnly: true, note: '가정 · 삽도 비율' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 지반·지층 입력 (2026-11-04): merges the given values into the record's 지반; undefined removes a value. Numbers must be finite.
|
||||
* The values are checked by the card (G01–G14), not refused here, as on the pier card.
|
||||
*/
|
||||
export function setAbutGround(rec: AbutRecord, patch: Partial<AbutGround>): AbutRecord {
|
||||
for (const [key, v] of Object.entries(patch)) {
|
||||
if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
if (key === 'strataRows' && (v as { bottomEl: number }[]).some((r) => !Number.isFinite(r.bottomEl))) throw new Error('지층 하단 EL은 숫자여야 합니다.');
|
||||
}
|
||||
const ground: AbutGround = { ...(rec.ground ?? {}), ...patch };
|
||||
for (const key of Object.keys(ground) as (keyof AbutGround)[]) if (ground[key] === undefined) delete ground[key];
|
||||
return { ...rec, ground };
|
||||
}
|
||||
|
||||
/** Changes one 접속슬래브 dim, refused with a Korean message when the slab would not build. */
|
||||
export function applySlabEdit(rec: AbutRecord, key: (typeof SLAB_EDIT_FIELDS)[number]['key'], value: number): AbutRecord {
|
||||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 교대 지반·지층 (2026-11-04, 시안 ② 지반·지층 카드 = 교각과 같은 카드). 순수 함수.
|
||||
*
|
||||
* - 입력은 교각과 같은 이름이다: 위치(육상 · 하상 · 해상), 지하수위, 설계홍수위 · 세굴, 고조위 · 저조위, 지층 행.
|
||||
* 지반고는 엑셀 입력(①에서 수정)이다. 표시만 하고 형상 계산에는 쓰지 않는다.
|
||||
* - 검사는 교각과 같다(G01–G14, 말뚝 근입 G04). 지반고가 저판 하면보다 낮으면 흙덮개가 없다는 정보 AI01을 더한다.
|
||||
*/
|
||||
import { SUPPORT_STRATUM, validateGround, validatePileEmbedment, validateStrataNValues, type GroundCheckInput } from '../ground';
|
||||
import type { Issue, PierInput } from '../types';
|
||||
import type { AbutMembers } from './members';
|
||||
import type { AbutRecord } from './parse';
|
||||
|
||||
export type AbutGround = Pick<PierInput, 'groundPosition' | 'groundWaterOn' | 'groundWaterEl' | 'floodLevelEl' | 'scourDepthM' | 'highWaterEl' | 'lowWaterEl' | 'strataRows'>;
|
||||
|
||||
/** 말뚝 두께 (mm): spec_abut 말뚝 Φ600 × 12T. Used only by the 해상 부식 여유 check. */
|
||||
const ABUT_PILE_THICKNESS_MM = 12;
|
||||
|
||||
/** The check input of an abutment: its 지반 values, 지반고 and 저판 하면 (기초 하단 when the 저판 is not built). */
|
||||
export function abutGroundCheckInput(rec: AbutRecord, members?: AbutMembers): GroundCheckInput {
|
||||
return {
|
||||
name: rec.name,
|
||||
groundEl: rec.groundEl,
|
||||
footingBottomEl: members?.footingBottomEl ?? rec.foundationBottomEl,
|
||||
pileThicknessMm: ABUT_PILE_THICKNESS_MM,
|
||||
...(rec.ground ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** 연암 상면 EL: the bottom of the row above the 연암 row (지반고 for the first row); undefined without a 연암 row. */
|
||||
export function supportTopEl(groundEl: number, rows: { name: string; bottomEl: number }[] | undefined): number | undefined {
|
||||
const list = rows ?? [];
|
||||
const k = list.findIndex((r) => r.name === SUPPORT_STRATUM);
|
||||
if (k < 0) return undefined;
|
||||
return k === 0 ? groundEl : list[k - 1].bottomEl;
|
||||
}
|
||||
|
||||
/** Issues of the 지반·지층 card: the pier checks plus AI01 (지반고 below the 저판 하면). */
|
||||
export function abutGroundIssues(rec: AbutRecord, members?: AbutMembers): Issue[] {
|
||||
const input = abutGroundCheckInput(rec, members);
|
||||
const issues: Issue[] = [];
|
||||
validateGround(input, issues);
|
||||
validateStrataNValues(input, issues);
|
||||
if (members && members.pileBottomEl !== undefined && members.piles.length > 0) {
|
||||
validatePileEmbedment(input, { piles: [{ bottom: members.pileBottomEl, diameter: members.shape.pileDiameterM }] }, issues);
|
||||
}
|
||||
if (rec.groundEl < input.footingBottomEl) {
|
||||
issues.push({
|
||||
code: 'AI01',
|
||||
level: 'I',
|
||||
pier: rec.name,
|
||||
message: `지반고 ${rec.groundEl.toFixed(3)}이 저판 하면 ${input.footingBottomEl.toFixed(3)}보다 ${(input.footingBottomEl - rec.groundEl).toFixed(3)} 낮아 흙덮개가 없습니다.`,
|
||||
});
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
/** True when any 지반 value differs from the default (육상, 지하수위 없음, 지층 행 없음). */
|
||||
export function abutGroundChanged(ground: AbutGround | undefined): boolean {
|
||||
if (!ground) return false;
|
||||
return Object.entries(ground).some(([key, v]) => {
|
||||
if (v === undefined) return false;
|
||||
if (key === 'groundPosition') return v !== '육상';
|
||||
if (key === 'groundWaterOn') return v === true;
|
||||
if (key === 'strataRows') return (v as unknown[]).length > 0;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { WingForm } from './geometry';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, wallThicknessMm, type AbutShape } from './shape';
|
||||
import { resolveSlab, SLAB_FORM_LABEL, slabChangedKeys, slabSlopeLabel, type AbutSlab } from './slab';
|
||||
import { abutGroundChanged, type AbutGround } from './ground';
|
||||
|
||||
/** 형상 입력 중 벽체·흉벽 카드와 확대기초·말뚝 카드의 항목 (변경 표시를 카드별로 나눈다). */
|
||||
const WALL_KEYS: (keyof AbutShape)[] = ['wallFrontMm', 'parapetHeightMm', 'parapetThicknessMm', 'bracketHeightMm', 'bracketWidthMm', 'shrinkJointCount', 'parapetLevelLeftMm', 'parapetLevelRightMm', 'seatStepIgnoreMm'];
|
||||
@@ -12,14 +13,14 @@ const FOOTING_KEYS: (keyof AbutShape)[] = [
|
||||
/** 부재 · 교대 list in ② (시안): the card each row opens. */
|
||||
export type AbutMemberId = 'bearing' | 'shape' | 'footing' | 'wing' | 'slab' | 'ground';
|
||||
|
||||
/** Cards that take input in this step; ground only shows its values (접속슬래브 입력 2026-11-04). */
|
||||
/** Cards that take input in this step: all six (접속슬래브 · 지반 입력 2026-11-04). */
|
||||
export const ABUT_MEMBER_EDITABLE: Record<AbutMemberId, boolean> = {
|
||||
bearing: true,
|
||||
shape: true,
|
||||
footing: true,
|
||||
wing: true,
|
||||
slab: true,
|
||||
ground: false,
|
||||
ground: true,
|
||||
};
|
||||
|
||||
const WING_LABEL: Record<WingForm, string> = { general: '일반형', short: '단축형', none: '없음' };
|
||||
@@ -30,6 +31,7 @@ export interface AbutMemberInput {
|
||||
shape?: Partial<AbutShape>;
|
||||
wings?: { left: WingForm; right: WingForm };
|
||||
slab?: Partial<AbutSlab>;
|
||||
ground?: AbutGround;
|
||||
groundEl: number;
|
||||
}
|
||||
|
||||
@@ -82,6 +84,11 @@ export function abutMemberRows(rec: AbutMemberInput, slabWidth: string): AbutMem
|
||||
summary: `${SLAB_FORM_LABEL[slab.form]} · ${slabSlopeLabel(slab)} · 폭 ${slabWidth}`,
|
||||
changed: slabChangedKeys(rec.slab).length > 0,
|
||||
},
|
||||
{ id: 'ground', label: '지반·지층', summary: `지반고 ${rec.groundEl.toFixed(3)} m`, changed: false },
|
||||
{
|
||||
id: 'ground',
|
||||
label: '지반·지층',
|
||||
summary: `${rec.ground?.groundPosition ?? '육상'} · 지층 ${rec.ground?.strataRows?.length ?? 0} · 지반고 EL.${rec.groundEl.toFixed(3)}`,
|
||||
changed: abutGroundChanged(rec.ground),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import type { AbutShape } from './shape';
|
||||
import type { AbutBearingRule } from './bearing-table';
|
||||
import type { WingDims } from './wings';
|
||||
import type { AbutSlab } from './slab';
|
||||
import type { AbutGround } from './ground';
|
||||
|
||||
type Cell = string | number | null | undefined;
|
||||
type Grid = Cell[][];
|
||||
@@ -62,6 +63,8 @@ export interface AbutRecord {
|
||||
wingDims?: { left?: Partial<WingDims>; right?: Partial<WingDims> };
|
||||
/** 접속슬래브 입력 (② 접속슬래브 카드: 형식, 경사, 치수 mm). Only the changed values. Absent = 매립형 · 하향 4 % 기본값. */
|
||||
slab?: Partial<AbutSlab>;
|
||||
/** 지반·지층 입력 (② 지반·지층 카드, 교각과 같은 필드). Absent = 육상, 지하수위 없음, 지층 행 없음. */
|
||||
ground?: AbutGround;
|
||||
/** 예전 "좌우 같게" 값 (2026-11-04 이전 파일). 지금은 읽기만 하고 쓰지 않는다. */
|
||||
wingsLinked?: boolean;
|
||||
}
|
||||
|
||||
+14
-6
@@ -1,6 +1,14 @@
|
||||
import type { PierModel } from './geometry';
|
||||
import type { Issue, PierInput } from './types';
|
||||
|
||||
/**
|
||||
* The values the 지반·지층 checks read (2026-11-04: shared by the pier and the abutment card). A pier passes its input;
|
||||
* an abutment passes its record's 지반 values with its 지반고 and 저판 하면.
|
||||
*/
|
||||
export type GroundCheckInput = Pick<
|
||||
PierInput,
|
||||
'name' | 'groundEl' | 'footingBottomEl' | 'groundPosition' | 'groundWaterOn' | 'groundWaterEl' | 'floodLevelEl' | 'scourDepthM' | 'highWaterEl' | 'lowWaterEl' | 'strataRows' | 'pileThicknessMm'
|
||||
>;
|
||||
|
||||
/** 위치 choices of the 지반·지층 card (시안 2026-10-26). */
|
||||
export const GROUND_POSITIONS = ['육상', '하상', '해상'] as const;
|
||||
|
||||
@@ -28,7 +36,7 @@ export const CORROSION_ALLOWANCE_MM = 1;
|
||||
* G03: 층 이름은 지층 목록에 있어야 함. 하상: G07 DHWL 없음, G08 DHWL이 지반고보다 낮음, G09 세굴선 확인.
|
||||
* Ground values are inputs only; they do not change the 3D model.
|
||||
*/
|
||||
export function validateGround(input: PierInput, issues: Issue[]): void {
|
||||
export function validateGround(input: GroundCheckInput, issues: Issue[]): void {
|
||||
const rows = input.strataRows ?? [];
|
||||
let upper = input.groundEl;
|
||||
rows.forEach((row, i) => {
|
||||
@@ -70,7 +78,7 @@ export function validateGround(input: PierInput, issues: Issue[]): void {
|
||||
* G04: a tip above that limit. G05 (info): piles but no 연암 row, so the embedment is not checked.
|
||||
* The 연암 상면 is the bottom of the row above the 연암 row (ground EL for the first row).
|
||||
*/
|
||||
export function validatePileEmbedment(input: PierInput, model: PierModel, issues: Issue[]): void {
|
||||
export function validatePileEmbedment(input: GroundCheckInput, model: { piles: { bottom: number; diameter: number }[] }, issues: Issue[]): void {
|
||||
if (model.piles.length === 0) return;
|
||||
const rows = input.strataRows ?? [];
|
||||
const k = rows.findIndex((r) => r.name === SUPPORT_STRATUM);
|
||||
@@ -123,7 +131,7 @@ export function parseNValue(text: string | undefined): NValue {
|
||||
* G06 (경고): an N value that is not a blow count or a refusal in the accepted range. Empty is allowed.
|
||||
* The values are not used in the calculation yet; they will feed the 지지력 and 액상화 checks, so the format is strict.
|
||||
*/
|
||||
export function validateStrataNValues(input: PierInput, issues: Issue[]): void {
|
||||
export function validateStrataNValues(input: GroundCheckInput, issues: Issue[]): void {
|
||||
(input.strataRows ?? []).forEach((row, i) => {
|
||||
if (parseNValue(row.nValue).kind === 'invalid') {
|
||||
issues.push({
|
||||
@@ -137,7 +145,7 @@ export function validateStrataNValues(input: PierInput, issues: Issue[]): void {
|
||||
}
|
||||
|
||||
/** G09: the 세굴 깊이 is not negative, and the scour line stays above the footing bottom (하상·해상). */
|
||||
function validateScour(input: PierInput, issues: Issue[]): void {
|
||||
function validateScour(input: GroundCheckInput, issues: Issue[]): void {
|
||||
const scour = input.scourDepthM;
|
||||
if (scour === undefined) return;
|
||||
if (!(scour >= 0)) {
|
||||
@@ -152,7 +160,7 @@ function validateScour(input: PierInput, issues: Issue[]): void {
|
||||
* 저조위 is below 고조위. G13 missing; G10 해저면이 고조위보다 낮지 않음; G11 저조위가 고조위보다 낮지 않음;
|
||||
* G14 강관 두께가 부식 여유 기준 이하. 파랑과 선박 하중은 이번 범위에서 제외.
|
||||
*/
|
||||
function validateSea(input: PierInput, issues: Issue[]): void {
|
||||
function validateSea(input: GroundCheckInput, issues: Issue[]): void {
|
||||
const hw = input.highWaterEl;
|
||||
const lw = input.lowWaterEl;
|
||||
if (hw === undefined || lw === undefined || !Number.isFinite(hw) || !Number.isFinite(lw)) {
|
||||
|
||||
@@ -150,6 +150,18 @@ export function checkStoredAbutments(list: unknown): void {
|
||||
if (!ok) throw new Error(`${who}: 접속슬래브 값 ${key}이(가) 맞지 않습니다.`);
|
||||
}
|
||||
}
|
||||
if (a.ground !== undefined) {
|
||||
const g = a.ground as Record<string, unknown>;
|
||||
for (const [key, v] of Object.entries(g)) {
|
||||
const ok =
|
||||
key === 'groundPosition' ? v === '육상' || v === '하상' || v === '해상'
|
||||
: key === 'groundWaterOn' ? typeof v === 'boolean'
|
||||
: key === 'strataRows'
|
||||
? Array.isArray(v) && v.every((r) => r && typeof r.name === 'string' && finite(r.bottomEl) && (r.nValue === undefined || typeof r.nValue === 'string'))
|
||||
: finite(v);
|
||||
if (!ok) throw new Error(`${who}: 지반 값 ${key}이(가) 맞지 않습니다.`);
|
||||
}
|
||||
}
|
||||
if (a.wingsLinked !== undefined && typeof a.wingsLinked !== 'boolean') throw new Error(`${who}: 좌우 같게 값이 맞지 않습니다.`);
|
||||
if (a.wings !== undefined) {
|
||||
const forms = ['general', 'short', 'none'];
|
||||
|
||||
Reference in new issue
Block a user