feat(abut): 교대 형상·입력 패널과 교각 받침·솔플레이트 개편
- 교대 형상(src/core/abut, 3D 장면, 목록·카메라·맞춤)과 교대 입력 패널 추가 - 교각 받침 기준 카드 제거, 솔플레이트 규칙을 교각별 입력으로 변경 - 받침 ELEVATION·솔플레이트 TABLE을 모델값으로 연결 - 교대 입력값 카드: 형식 행에 시점·종점 표시, 제목을 "A1 입력값 [입력]"으로 변경 - 테스트 614건 통과, 타입 검사 통과 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1b2ad1ddb
commit
1d31d2f3a9
44 files changed
+2571
-120
No files matched your search
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 교대 입력 편집 (2026-11-03, 교대 편집 카드): changes one value of an abutment record with checks. Pure: it returns a new
|
||||
* record and never changes the old one. Errors are in Korean and name the field.
|
||||
*/
|
||||
import type { AbutRecord } from './parse';
|
||||
import type { WingForm } from './geometry';
|
||||
import { shapeIssues, type AbutShape } from './shape';
|
||||
|
||||
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
|
||||
|
||||
export const ABUT_EDIT_FIELDS: { key: AbutEditKey; label: string; unit: string }[] = [
|
||||
{ key: 'seatEl', label: '교좌면 EL (구간 ①)', unit: 'm' },
|
||||
{ key: 'foundationBottomEl', label: '기초 하단 EL', unit: 'm' },
|
||||
{ key: 'groundEl', label: '지반고 EL', unit: 'm' },
|
||||
{ key: 'pileLengthMm', label: '말뚝길이', unit: 'mm' },
|
||||
{ key: 'skewDeg', label: 'skew', unit: '°' },
|
||||
{ key: 'slopeI', label: '종단경사 i', unit: '' },
|
||||
];
|
||||
|
||||
/** The 저판 thickness (m) below the 기초 하단 is not part of this check; the 교좌면 must sit above the 기초 하단. */
|
||||
export function applyAbutEdit(rec: AbutRecord, key: AbutEditKey, value: number): AbutRecord {
|
||||
const label = ABUT_EDIT_FIELDS.find((f) => f.key === key)?.label ?? key;
|
||||
if (!Number.isFinite(value)) throw new Error(`${label}은(는) 숫자여야 합니다.`);
|
||||
if (key === 'pileLengthMm' && !(value > 0)) throw new Error(`${label}은(는) 0보다 커야 합니다.`);
|
||||
if (key === 'skewDeg' && !(value > 0 && value < 180)) throw new Error(`${label}은(는) 0 초과 180 미만이어야 합니다.`);
|
||||
if (key === 'slopeI' && Math.abs(value) > 0.2) throw new Error(`${label}은(는) ±0.2 이내여야 합니다.`);
|
||||
const next: AbutRecord = { ...rec, [key]: value };
|
||||
if (next.seatEl <= next.foundationBottomEl) {
|
||||
throw new Error('교좌면 EL은 기초 하단 EL보다 높아야 합니다.');
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function setWingForm(rec: AbutRecord, side: 'left' | 'right', form: WingForm): AbutRecord {
|
||||
const current = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||||
return { ...rec, wings: { ...current, [side]: form } };
|
||||
}
|
||||
|
||||
/** 받침 값 편집 (2026-11-03): z (패드 상면, m) or 교좌면 override (m, undefined = 자동 — 블럭 높이 규칙을 따른다). */
|
||||
export function applyBearingEdit(rec: AbutRecord, no: number, key: 'z' | 'seatEl', value: number | undefined): AbutRecord {
|
||||
const target = rec.bearings.find((b) => b.no === no);
|
||||
if (!target) throw new Error(`받침 ${no}번이 없습니다.`);
|
||||
if (key === 'z' && (value === undefined || !Number.isFinite(value))) throw new Error('z는 숫자여야 합니다.');
|
||||
if (key === 'seatEl' && value !== undefined && !Number.isFinite(value)) throw new Error('교좌면 EL은 숫자이거나 비워야 합니다.');
|
||||
const next = rec.bearings.map((b) => {
|
||||
if (b.no !== no) return b;
|
||||
if (key === 'z') return { ...b, z: value as number };
|
||||
return value === undefined ? { ...b, seatEl: undefined } : { ...b, seatEl: value };
|
||||
});
|
||||
const out: AbutRecord = { ...rec, bearings: next };
|
||||
// The 교좌면 of every bearing must still lie above the 기초 하단 (the 저판 thickness is checked in the geometry step).
|
||||
const lowest = Math.min(...out.bearings.map((b) => b.seatEl ?? out.seatEl + (b.z - Math.min(...out.bearings.map((x) => x.z)))));
|
||||
if (lowest <= out.foundationBottomEl) throw new Error(`받침 ${no}번 교좌면 EL이 기초 하단 EL보다 낮거나 같습니다.`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 기본값 복원: the record as imported from the workbook (the wing forms go back to 양측 일반형). */
|
||||
export function restoreAbutDefault(defaults: AbutRecord): AbutRecord {
|
||||
return JSON.parse(JSON.stringify(defaults)) as AbutRecord;
|
||||
}
|
||||
|
||||
/** 형상 입력 change (벽체·흉벽, 확대기초·말뚝, mm): refused with a Korean message when the shape would not build. */
|
||||
export function applyShapeEdit(rec: AbutRecord, key: keyof AbutShape, value: number): AbutRecord {
|
||||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||||
const shape = { ...(rec.shape ?? {}), [key]: value };
|
||||
const issues = shapeIssues(shape);
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, shape };
|
||||
}
|
||||
|
||||
|
||||
/** 형상 입력 fields on the 교대 카드 (시안 ② 벽체·흉벽, 확대기초·말뚝), in the order of the mockup. */
|
||||
export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string }[] = [
|
||||
{ key: 'wallFrontMm', label: '벽체 전면 (교대축 기준, mm)' },
|
||||
{ key: 'wallThicknessMm', label: '벽체 두께 (mm)' },
|
||||
{ key: 'parapetHeightMm', label: '흉벽 높이 (mm)' },
|
||||
{ key: 'parapetThicknessMm', label: '흉벽 두께 (mm)' },
|
||||
{ key: 'bracketHeightMm', label: '브라켓 높이 (mm)' },
|
||||
{ key: 'footingThicknessMm', label: '저판 두께 (mm)' },
|
||||
{ key: 'footingFrontMm', label: '저판 앞굽 끝 (mm)' },
|
||||
{ key: 'footingBackMm', label: '저판 뒷굽 끝 (mm)' },
|
||||
{ key: 'pileEdgeMm', label: '말뚝 연단 (mm)' },
|
||||
{ key: 'pileSpacingMm', label: '말뚝 간격 (mm)' },
|
||||
];
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 교대 형상 · AA1 (2026-11-03, spec_abut §1·§4.1–4.2): 교대축, 전면 쪽, 교좌면 위의 받침, 교좌면 EL과 블럭 높이를 계산한다.
|
||||
* 순수 함수. 좌표는 (E, N) m 단위, EL은 m 단위, 길이는 별도 표기가 없으면 mm 단위다.
|
||||
*
|
||||
* - 축: IP1 → IP3 선(교대중심선), 길이 L. u = IP1 → IP3 단위벡터, nLeft = u를 반시계 90° 돌린 벡터.
|
||||
* - 받침 s: IP1에서 u 방향으로 잰 거리(m). 축에서 벗어난 거리(lateral): nLeft 방향 거리(m).
|
||||
* - z = 패드 상면(EL.X), 교각과 같다(2026-11-03 결정 1). 교좌면에서 패드 상면까지 블럭 높이는 125 mm
|
||||
* (콘크리트 75 + 패드 50). 교좌면 EL은 콘크리트 하면이다.
|
||||
* - 교좌면: 교대 전체에 교좌면 EL 하나를 입력받는다(첫 받침 ①). 블럭 높이 h = min(z) − 교좌면으로 모든 받침이 같고,
|
||||
* 받침 i의 교좌면 = z_i − h. 받침이 자기 교좌면 EL(seatEl)을 직접 주면 그 값을 쓰고, 블럭 높이는 z_i − seatEl이 된다
|
||||
* (2026-11-03 결정 3).
|
||||
* - 전면: 받침이 있는 쪽. A1은 전면이 1 → n 방향의 왼쪽, 흉벽은 오른쪽에 있다. A2는 반대다(spec_abut §1).
|
||||
* 맞지 않으면 AW03 경고 대상이다.
|
||||
*/
|
||||
|
||||
export type AbutType = 'A1' | 'A2';
|
||||
|
||||
export interface AbutPoint {
|
||||
e: number;
|
||||
n: number;
|
||||
}
|
||||
|
||||
export interface AbutBearing {
|
||||
no: number;
|
||||
/** 위치 (E, N), m. */
|
||||
pos: AbutPoint;
|
||||
/** 패드 상면 EL (z), m. */
|
||||
z: number;
|
||||
/** 이 받침의 교좌면 EL (m). 없으면 z에서 교대 블럭 높이를 뺀 값을 쓴다. */
|
||||
seatEl?: number;
|
||||
}
|
||||
|
||||
/** 날개벽 형식, 측별(spec_abut §4.7): 일반형 / 단축형 / 없음. */
|
||||
export type WingForm = 'general' | 'short' | 'none';
|
||||
|
||||
export interface AbutInput {
|
||||
name: string;
|
||||
type: AbutType;
|
||||
/** 첫 받침(구간 ①)의 교좌면 EL (m). 블럭 높이 h = min(z) − seatEl. */
|
||||
seatEl: number;
|
||||
ip1: AbutPoint;
|
||||
ip2: AbutPoint;
|
||||
ip3: AbutPoint;
|
||||
bearings: AbutBearing[];
|
||||
/** skew (deg): 받침축을 시계방향으로 돌린 선이 날개벽 선이다(spec_abut §1). 없으면 날개벽 검사를 하지 않는다. */
|
||||
skewDeg?: number;
|
||||
/** 종단경사 i (소수, 교량진행방향 기준, (−) = 낮아짐). 날개벽 경사는 A1이 −i, A2가 +i다. */
|
||||
slopeI?: number;
|
||||
/** AW01용 거더 방향 CAD 각 (deg). 없으면 AW01을 검사하지 않는다. */
|
||||
girderAzimuthDeg?: number;
|
||||
/** 측별 날개벽 형식: 'left' = 받침 ① 쪽 끝, 'right' = 받침 n 쪽 끝(spec §4.7). 기본은 양측 일반형. */
|
||||
wings?: { left: WingForm; right: WingForm };
|
||||
/** 기초 하단 EL (m), 말뚝길이 (mm), 기초 형식 (AA3, 저판·말뚝). 없으면 저판·말뚝을 만들지 않는다. */
|
||||
foundationBottomEl?: number;
|
||||
pileLengthMm?: number;
|
||||
foundationType?: string;
|
||||
}
|
||||
|
||||
export interface AbutWingModel {
|
||||
/** 날개벽 선, CAD 각 (deg), 배면 쪽(예제: A1 ≈ 255.32, A2 ≈ 75.32). */
|
||||
lineDeg: number;
|
||||
/** 날개벽 선과 거더 방향의 사잇각, 0–90°로 접음(AW01 허용 1°). */
|
||||
girderDiffDeg: number;
|
||||
aw01Ok: boolean;
|
||||
/** 날개벽 경사 (%), (+) = 벽체에서 멀어질수록 높아짐. */
|
||||
slopePct: number;
|
||||
/** 날개벽 사이 접속슬래브 폭 (mm) = L − Σ(450 / |sin Δ| + 20), 있는 날개벽마다. */
|
||||
slabWidthMm: number;
|
||||
wings: { left: WingForm; right: WingForm };
|
||||
}
|
||||
|
||||
export interface AbutBearingModel {
|
||||
no: number;
|
||||
/** IP1에서 u 방향 거리 (m). */
|
||||
s: number;
|
||||
/** nLeft 방향으로 잰 축에서의 부호 있는 거리 (m). */
|
||||
lateral: number;
|
||||
z: number;
|
||||
seatEl: number;
|
||||
/** 교좌면에서 패드 상면까지 블럭 높이 (mm), 패드 포함. */
|
||||
blockHeightMm: number;
|
||||
}
|
||||
|
||||
export interface AbutModel {
|
||||
name: string;
|
||||
type: AbutType;
|
||||
/** IP1 (축의 시작점, s = 0). */
|
||||
ip1: AbutPoint;
|
||||
/** 축 길이 IP1 → IP3 (m). */
|
||||
length: number;
|
||||
u: AbutPoint;
|
||||
nLeft: AbutPoint;
|
||||
/** 받침이 축의 nLeft 쪽이면 +1, 반대쪽이면 −1. */
|
||||
frontSide: 1 | -1;
|
||||
/** 받침이 1 → n 방향의 왼쪽이면 true (A1 배치). */
|
||||
frontLeftOfBearingDir: boolean;
|
||||
/** 입력 순서의 받침. */
|
||||
bearings: AbutBearingModel[];
|
||||
/** 축 위 계단 경계(IP1에서 m), 이웃 받침 사이의 중간. */
|
||||
stepBounds: number[];
|
||||
/** AW03: 배치가 형식과 맞으면 true. */
|
||||
layoutMatchesType: boolean;
|
||||
/** 날개벽 선, 경사, AW01, 접속슬래브 폭. 입력에 skew가 없으면 없다. */
|
||||
wing?: AbutWingModel;
|
||||
}
|
||||
|
||||
const WING_THICKNESS_MM = 450;
|
||||
const SLAB_CLEARANCE_MM = 20;
|
||||
const AW01_TOLERANCE_DEG = 1;
|
||||
|
||||
/** 공통 규칙의 패드 두께 (mm). 블럭 높이에 포함된다(패드 포함). */
|
||||
export const ABUT_PAD_MM = 50;
|
||||
|
||||
const sub = (a: AbutPoint, b: AbutPoint): AbutPoint => ({ e: a.e - b.e, n: a.n - b.n });
|
||||
const dot = (a: AbutPoint, b: AbutPoint) => a.e * b.e + a.n * b.n;
|
||||
const cross = (a: AbutPoint, b: AbutPoint) => a.e * b.n - a.n * b.e;
|
||||
|
||||
export function buildAbut(input: AbutInput): AbutModel {
|
||||
if (input.bearings.length < 2) throw new Error(`교대 ${input.name}: 받침이 2개 이상 필요합니다.`);
|
||||
const axisVec = sub(input.ip3, input.ip1);
|
||||
const length = Math.hypot(axisVec.e, axisVec.n);
|
||||
if (!(length > 0)) throw new Error(`교대 ${input.name}: IP1과 IP3이 같은 점입니다.`);
|
||||
const u = { e: axisVec.e / length, n: axisVec.n / length };
|
||||
const nLeft = { e: -u.n, n: u.e };
|
||||
|
||||
const lateral = input.bearings.map((b) => dot(sub(b.pos, input.ip1), nLeft));
|
||||
const meanLateral = lateral.reduce((a, x) => a + x, 0) / lateral.length;
|
||||
const frontSide: 1 | -1 = meanLateral >= 0 ? 1 : -1;
|
||||
|
||||
// 1 → n 방향: 첫 받침(①)에서 마지막 받침까지. 전면 = 받침 쪽. 형식과 맞는지 확인한다.
|
||||
const first = input.bearings[0];
|
||||
const last = input.bearings[input.bearings.length - 1];
|
||||
const dir = sub(last.pos, first.pos);
|
||||
const frontVec = { e: nLeft.e * frontSide, n: nLeft.n * frontSide };
|
||||
const frontLeftOfBearingDir = cross(dir, frontVec) > 0;
|
||||
const layoutMatchesType = input.type === 'A1' ? frontLeftOfBearingDir : !frontLeftOfBearingDir;
|
||||
|
||||
const minZ = Math.min(...input.bearings.map((b) => b.z));
|
||||
const bearings: AbutBearingModel[] = input.bearings.map((b, i) => {
|
||||
const s = dot(sub(b.pos, input.ip1), u);
|
||||
const seatEl = b.seatEl ?? input.seatEl + (b.z - minZ);
|
||||
const blockHeightMm = Math.round((b.z - seatEl) * 1000);
|
||||
return { no: b.no, s, lateral: lateral[i], z: b.z, seatEl, blockHeightMm };
|
||||
});
|
||||
|
||||
const sorted = [...bearings].sort((a, b) => a.s - b.s);
|
||||
const stepBounds = sorted.slice(1).map((b, i) => (sorted[i].s + b.s) / 2);
|
||||
|
||||
const wing = input.skewDeg === undefined ? undefined : buildWing(input, u, nLeft, frontSide, dir, length);
|
||||
return { name: input.name, type: input.type, ip1: input.ip1, length, u, nLeft, frontSide, frontLeftOfBearingDir, bearings, stepBounds, layoutMatchesType, wing };
|
||||
}
|
||||
|
||||
const deg = (rad: number) => (rad * 180) / Math.PI;
|
||||
const norm360 = (x: number) => ((x % 360) + 360) % 360;
|
||||
/** 두 선 방향의 차이(deg), 0–90°로 접는다. 선에는 방향이 없다. */
|
||||
const lineDiff = (a: number, b: number) => {
|
||||
const d = norm360(a - b) % 180;
|
||||
return Math.min(d, 180 - d);
|
||||
};
|
||||
|
||||
function buildWing(input: AbutInput, u: AbutPoint, nLeft: AbutPoint, frontSide: 1 | -1, dir: AbutPoint, length: number): AbutWingModel {
|
||||
const skew = input.skewDeg!;
|
||||
const bearingCad = norm360(deg(Math.atan2(dir.n, dir.e)));
|
||||
// skew만큼 시계방향으로 돌린 선이 날개벽 선이다. 배면 쪽(받침 반대쪽)이 방향을 정한다.
|
||||
const line = norm360(bearingCad - skew);
|
||||
const back = { e: -nLeft.e * frontSide, n: -nLeft.n * frontSide };
|
||||
const radLine = (line * Math.PI) / 180;
|
||||
const pick = Math.cos(radLine) * back.e + Math.sin(radLine) * back.n > 0 ? line : norm360(line + 180);
|
||||
const axisCad = norm360(deg(Math.atan2(u.n, u.e)));
|
||||
const sinDelta = Math.abs(Math.sin(((pick - axisCad) * Math.PI) / 180));
|
||||
const wingAxisWidth = WING_THICKNESS_MM / sinDelta;
|
||||
const wings = input.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||||
const present = (f: WingForm) => (f === 'none' ? 0 : wingAxisWidth + SLAB_CLEARANCE_MM);
|
||||
const slabWidthMm = Math.round(length * 1000 - present(wings.left) - present(wings.right));
|
||||
const girderDiffDeg = input.girderAzimuthDeg === undefined ? NaN : lineDiff(pick, input.girderAzimuthDeg);
|
||||
const slopeI = input.slopeI ?? 0;
|
||||
return {
|
||||
lineDeg: pick,
|
||||
girderDiffDeg,
|
||||
aw01Ok: Number.isNaN(girderDiffDeg) ? true : girderDiffDeg <= AW01_TOLERANCE_DEG,
|
||||
slopePct: (input.type === 'A1' ? -slopeI : slopeI) * 100,
|
||||
slabWidthMm,
|
||||
wings,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 교대 부재 · AA3 (2026-11-03, spec_abut §4.3–4.6): 벽체, 흉벽 상단, 저판, 말뚝의 치수와 위치. Pure.
|
||||
*
|
||||
* - 교대축 기준 t: 전면 쪽이 +. 벽체 전면 +1.6 m (교좌면 끝), 배면 −0.8 m (브라켓 배면). 두께 2.4 m.
|
||||
* - 흉벽: t −0.5 ~ 0 m. 상단 EL(s) = 교좌면 EL(구간 ①) + 흉벽 높이 2.5 m + i_b × (s − s_1), i_b = 받침 z의 축 방향 경사.
|
||||
* - 저판: 하면 = 기초 하단 EL, 두께 1.1 m. 폭은 t −1.8 ~ +2.8 m (앞굽 1.2, 뒷굽 1.0, 벽체 2.4). 길이 = 교좌면 길이 L.
|
||||
* - 말뚝: 열 위치 t = −1.15 / +0.85 / +2.15 m. 열 안 본수 = int((L − 1.3)/2.0) + 1, 양끝 0.65 m 띄움, 균등 간격.
|
||||
* 말뚝 선단 EL = 기초 하단 EL − 말뚝길이. (skew 평행사변형에서 열 끝이 밀리는 보정은 아직 없다 — QA-5 이후.)
|
||||
* - 벽체 높이 = 구간 교좌면 EL − (기초 하단 EL + 저판 두께).
|
||||
*/
|
||||
import type { AbutModel } from './geometry';
|
||||
import { resolveShape, type AbutShape, type ResolvedShape } from './shape';
|
||||
|
||||
export interface WallSegment {
|
||||
/** 축 위 구간 범위 (IP1에서 m). */
|
||||
sFrom: number;
|
||||
sTo: number;
|
||||
/** 구간의 교좌면 EL (구간 i). */
|
||||
seatEl: number;
|
||||
/** 벽체 높이 (m) = seat − (footing bottom + footing thickness). */
|
||||
heightM: number;
|
||||
}
|
||||
|
||||
export interface PileRow {
|
||||
/** 횡방향 위치 t (m, 전면 +). */
|
||||
t: number;
|
||||
/** 축 위 말뚝 위치 (IP1에서 m). */
|
||||
s: number[];
|
||||
}
|
||||
|
||||
export interface AbutMembers {
|
||||
/** The shape values in use (defaults merged with the user's changes), in m. */
|
||||
shape: ResolvedShape;
|
||||
wallSegments: WallSegment[];
|
||||
/** 흉벽 상단 EL: s = 0 (IP1 끝)과 s = L (IP3 끝), 축 방향 경사 i_b (소수, + = IP3 쪽으로 올라감). */
|
||||
parapetTopStartEl: number;
|
||||
parapetTopEndEl: number;
|
||||
parapetSlope: number;
|
||||
footingTopEl: number;
|
||||
footingBottomEl: number | undefined;
|
||||
piles: PileRow[];
|
||||
pileBottomEl: number | undefined;
|
||||
}
|
||||
|
||||
/** 흉벽 상단 EL at axial position s (m). */
|
||||
export function parapetTopEl(seatFirst: number, bearingSFirst: number, slope: number, s: number, parapetHeightM: number): number {
|
||||
return seatFirst + parapetHeightM + slope * (s - bearingSFirst);
|
||||
}
|
||||
|
||||
export function buildMembers(model: AbutModel, foundation: { bottomEl?: number; pileLengthMm?: number } = {}, shapeChanges?: Partial<AbutShape>): AbutMembers {
|
||||
const L = model.length;
|
||||
const shape = resolveShape(shapeChanges);
|
||||
const sorted = [...model.bearings].sort((a, b) => a.s - b.s);
|
||||
const bounds = [0, ...model.stepBounds, L];
|
||||
const footingBottomEl = foundation.bottomEl;
|
||||
const footingTopEl = footingBottomEl === undefined ? NaN : footingBottomEl + shape.footingThicknessM;
|
||||
|
||||
const wallSegments: WallSegment[] = sorted.map((b, k) => ({
|
||||
sFrom: bounds[k],
|
||||
sTo: bounds[k + 1],
|
||||
seatEl: b.seatEl,
|
||||
heightM: b.seatEl - footingTopEl,
|
||||
}));
|
||||
|
||||
// 받침 ①(첫 입력)과 마지막 입력 받침이 흉벽 상단선을 정한다 (spec §4.4).
|
||||
const first = model.bearings[0];
|
||||
const last = model.bearings[model.bearings.length - 1];
|
||||
const slope = (last.z - first.z) / (last.s - first.s);
|
||||
const parapetTopStartEl = parapetTopEl(first.seatEl, first.s, slope, 0, shape.parapetHeightM);
|
||||
const parapetTopEndEl = parapetTopEl(first.seatEl, first.s, slope, L, shape.parapetHeightM);
|
||||
|
||||
const count = Math.floor((L - 2 * shape.pileEdgeM) / shape.pileSpacingM) + 1;
|
||||
const spacing = count > 1 ? (L - 2 * shape.pileEdgeM) / (count - 1) : 0;
|
||||
const piles: PileRow[] = shape.pileRowsT.map((t) => ({
|
||||
t,
|
||||
s: Array.from({ length: count }, (_, k) => shape.pileEdgeM + k * spacing),
|
||||
}));
|
||||
const pileBottomEl = footingBottomEl === undefined || foundation.pileLengthMm === undefined ? undefined : footingBottomEl - foundation.pileLengthMm / 1000;
|
||||
|
||||
return {
|
||||
shape,
|
||||
wallSegments,
|
||||
parapetTopStartEl,
|
||||
parapetTopEndEl,
|
||||
parapetSlope: slope,
|
||||
footingTopEl,
|
||||
footingBottomEl,
|
||||
piles,
|
||||
pileBottomEl,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 구조물 목록 순서 (2026-11-03, 교대 AB): 시점 교대(A1) → 교각 P1 … Pn → 종점 교대(A2), 교대가 여럿이면 번호 순.
|
||||
* Pure. Used by the 구조물 목록 so the abutments sit at the two ends of the bridge.
|
||||
*/
|
||||
export interface StructureRef {
|
||||
kind: 'pier' | 'abut';
|
||||
name: string;
|
||||
/** 교대 형식 (A1 · A2), only for abutments. */
|
||||
type?: 'A1' | 'A2';
|
||||
}
|
||||
|
||||
const numberOf = (name: string) => Number(name.replace(/\D/g, '')) || 0;
|
||||
|
||||
export function structureOrder(abutments: { name: string; type: 'A1' | 'A2' }[], piers: string[]): StructureRef[] {
|
||||
const start = abutments.filter((a) => a.type === 'A1').sort((a, b) => numberOf(a.name) - numberOf(b.name));
|
||||
const end = abutments.filter((a) => a.type === 'A2').sort((a, b) => numberOf(a.name) - numberOf(b.name));
|
||||
const others = abutments.filter((a) => a.type !== 'A1' && a.type !== 'A2');
|
||||
return [
|
||||
...start.map((a) => ({ kind: 'abut' as const, name: a.name, type: a.type })),
|
||||
...piers.map((name) => ({ kind: 'pier' as const, name })),
|
||||
...end.map((a) => ({ kind: 'abut' as const, name: a.name, type: a.type })),
|
||||
...others.map((a) => ({ kind: 'abut' as const, name: a.name, type: a.type })),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 교대 입력 읽기 (2026-11-03, spec_abut §2): 입력 엑셀의 교대 행을 읽는다. 교대 표는 교각 표와 같은 시트에 있으며,
|
||||
* 첫 두 헤더 칸(교대명 + 형식 / IP점 / 받침번호)으로 구분한다. 워크북 객체에 대해 순수하다. 좌표: 시트 x_m = 북, y_m = 동 → 내부 (E, N) = (y_m, x_m).
|
||||
*
|
||||
* 받침높이 (2026-11-03 결정 1): 교각과 같은 정의 — 패드 상면(z)에서 솔플레이트 상면까지.
|
||||
* 받침 행의 값을 그대로 읽는다(A1 시트 값 275). 여기서 계산하지 않는다.
|
||||
*/
|
||||
import * as XLSX from 'xlsx';
|
||||
import { parseDms } from '../dms';
|
||||
import type { Issue } from '../types';
|
||||
import type { AbutInput, AbutPoint, AbutType, WingForm } from './geometry';
|
||||
import type { AbutShape } from './shape';
|
||||
|
||||
type Cell = string | number | null | undefined;
|
||||
type Grid = Cell[][];
|
||||
|
||||
export interface AbutRecordBearing {
|
||||
no: number;
|
||||
/** Position (E, N), m: E = y_m, N = x_m. */
|
||||
pos: AbutPoint;
|
||||
z: number;
|
||||
/** 교좌면_EL of this bearing (m). Absent = the block rule (z − 블럭 높이). */
|
||||
seatEl?: number;
|
||||
/** 이 받침에 적힌 받침높이 (mm). */
|
||||
bearingHeightMm: number;
|
||||
row: string;
|
||||
}
|
||||
|
||||
export interface AbutRecord {
|
||||
name: string;
|
||||
sheet: string;
|
||||
type: AbutType;
|
||||
/** 첫 받침(구간 ①)의 교좌면 EL (m). */
|
||||
seatEl: number;
|
||||
/** 기초 하단 EL (m), 지반고 EL (m), 기초 형식, 말뚝길이 (mm). */
|
||||
foundationBottomEl: number;
|
||||
groundEl: number;
|
||||
foundation: string;
|
||||
pileLengthMm: number;
|
||||
/** skew (deg) from the DMS text, and 종단경사 i (decimal). */
|
||||
skewDeg: number;
|
||||
slopeI: number;
|
||||
ip1: AbutPoint;
|
||||
ip2: AbutPoint;
|
||||
ip3: AbutPoint;
|
||||
bearings: AbutRecordBearing[];
|
||||
/** 받침 행에서 읽은 받침높이 (mm). 모든 받침이 같아야 하며, 다르면 AP07 경고. */
|
||||
bearingHeightMm: number;
|
||||
/** 측별 날개벽 형식 (편집 카드에서 정함). Absent = 양측 일반형. */
|
||||
wings?: { left: WingForm; right: WingForm };
|
||||
/** 형상 입력 (벽체·흉벽, 확대기초·말뚝, mm). Only the changed values; the rest are the defaults. Absent = all defaults. */
|
||||
shape?: Partial<AbutShape>;
|
||||
}
|
||||
|
||||
export interface AbutParseResult {
|
||||
abutments: AbutRecord[];
|
||||
issues: Issue[];
|
||||
}
|
||||
|
||||
const num = (c: Cell) => (c === null || c === undefined || c === '' ? NaN : Number(c));
|
||||
const isBlank = (c: Cell) => c === null || c === undefined || c === '';
|
||||
|
||||
/** 교대 표의 헤더 행 (첫 두 칸). */
|
||||
const isAbutInfoHeader = (r: Cell[]) => r[0] === '교대명' && r[1] === '형식';
|
||||
const isAbutIpHeader = (r: Cell[]) => r[0] === '교대명' && r[1] === 'IP점';
|
||||
const isAbutBearingHeader = (r: Cell[]) => r[0] === '교대명' && r[1] === '받침번호';
|
||||
|
||||
/** 헤더 아래 데이터 행, 둘째 칸이 빌 때까지. 교대명 칸이 비면 위 행을 이어받는다. */
|
||||
function readBlock(grid: Grid, header: number): { cells: Cell[]; row: number }[] {
|
||||
const out: { cells: Cell[]; row: number }[] = [];
|
||||
let last: Cell = null;
|
||||
for (let i = header + 1; i < grid.length; i++) {
|
||||
const r = grid[i] ?? [];
|
||||
if (isBlank(r[1])) break;
|
||||
const name: Cell = isBlank(r[0]) ? last : r[0];
|
||||
last = name;
|
||||
out.push({ cells: [name, ...r.slice(1)], row: i + 1 });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseAbutments(wb: XLSX.WorkBook): AbutParseResult {
|
||||
const issues: Issue[] = [];
|
||||
const info: { cells: Cell[]; row: number; sheet: string }[] = [];
|
||||
const ips: { cells: Cell[]; row: number; sheet: string }[] = [];
|
||||
const bearings: { cells: Cell[]; row: number; sheet: string }[] = [];
|
||||
|
||||
for (const sheet of wb.SheetNames) {
|
||||
const grid = XLSX.utils.sheet_to_json<Cell[]>(wb.Sheets[sheet], { header: 1, raw: true, defval: null }) as Grid;
|
||||
grid.forEach((r, i) => {
|
||||
const pick = (match: (r: Cell[]) => boolean, into: typeof info) => {
|
||||
if (!match(r)) return;
|
||||
for (const rec of readBlock(grid, i)) into.push({ ...rec, sheet });
|
||||
};
|
||||
pick(isAbutInfoHeader, info);
|
||||
pick(isAbutIpHeader, ips);
|
||||
pick(isAbutBearingHeader, bearings);
|
||||
});
|
||||
}
|
||||
|
||||
const abutments: AbutRecord[] = [];
|
||||
for (const rec of info) {
|
||||
const c = rec.cells;
|
||||
const name = String(c[0] ?? '').trim();
|
||||
const where = `${rec.sheet} ${rec.row}행`;
|
||||
if (!/^A\d+$/.test(name)) {
|
||||
issues.push({ code: 'AP01', level: 'E', sheet: rec.sheet, message: `${where}: 교대명 "${String(c[0] ?? '')}"이 올바르지 않습니다 (A1, A2 …).` });
|
||||
continue;
|
||||
}
|
||||
const type = String(c[1] ?? '').trim();
|
||||
if (type !== 'A1' && type !== 'A2') {
|
||||
issues.push({ code: 'AP02', level: 'E', pier: name, sheet: rec.sheet, message: `${where}: 교대 ${name}의 형식 "${type}"은 A1 또는 A2여야 합니다.` });
|
||||
continue;
|
||||
}
|
||||
const skewDeg = parseDms(String(c[7] ?? ''));
|
||||
if (skewDeg === null) {
|
||||
issues.push({ code: 'AP03', level: 'E', pier: name, sheet: rec.sheet, message: `${where}: 교대 ${name}의 skew "${String(c[7] ?? '')}"를 읽지 못했습니다.` });
|
||||
continue;
|
||||
}
|
||||
abutments.push({
|
||||
name,
|
||||
sheet: rec.sheet,
|
||||
type,
|
||||
seatEl: num(c[2]),
|
||||
foundationBottomEl: num(c[3]),
|
||||
groundEl: num(c[4]),
|
||||
foundation: String(c[5] ?? ''),
|
||||
pileLengthMm: num(c[6]),
|
||||
skewDeg,
|
||||
slopeI: num(c[8]),
|
||||
ip1: { e: NaN, n: NaN },
|
||||
ip2: { e: NaN, n: NaN },
|
||||
ip3: { e: NaN, n: NaN },
|
||||
bearings: [],
|
||||
bearingHeightMm: NaN,
|
||||
});
|
||||
}
|
||||
|
||||
for (const rec of ips) {
|
||||
const name = String(rec.cells[0] ?? '').trim();
|
||||
const ab = abutments.find((a) => a.name === name);
|
||||
if (!ab) {
|
||||
issues.push({ code: 'AP04', level: 'E', sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 교대명 ${name}의 교대 정보 행이 없습니다.` });
|
||||
continue;
|
||||
}
|
||||
const point = { e: num(rec.cells[3]), n: num(rec.cells[2]) };
|
||||
const ipName = String(rec.cells[1] ?? '');
|
||||
if (ipName === 'IP1') ab.ip1 = point;
|
||||
else if (ipName === 'IP2') ab.ip2 = point;
|
||||
else if (ipName === 'IP3') ab.ip3 = point;
|
||||
}
|
||||
|
||||
for (const rec of bearings) {
|
||||
const c = rec.cells;
|
||||
const name = String(c[0] ?? '').trim();
|
||||
const ab = abutments.find((a) => a.name === name);
|
||||
if (!ab) {
|
||||
issues.push({ code: 'AP04', level: 'E', sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 교대명 ${name}의 교대 정보 행이 없습니다.` });
|
||||
continue;
|
||||
}
|
||||
const h = num(c[8]);
|
||||
ab.bearings.push({
|
||||
no: num(c[1]),
|
||||
pos: { e: num(c[3]), n: num(c[2]) },
|
||||
z: num(c[4]),
|
||||
seatEl: isBlank(c[7]) ? undefined : num(c[7]),
|
||||
bearingHeightMm: h,
|
||||
row: `${rec.sheet} ${rec.row}행`,
|
||||
});
|
||||
}
|
||||
|
||||
for (const ab of abutments) {
|
||||
const missing = [ab.ip1, ab.ip2, ab.ip3].some((p) => !Number.isFinite(p.e) || !Number.isFinite(p.n));
|
||||
if (missing) issues.push({ code: 'AP05', level: 'E', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: IP1·IP2·IP3 중 빠진 점이 있습니다.` });
|
||||
if (ab.bearings.length < 2) issues.push({ code: 'AP06', level: 'E', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: 받침이 2개 이상 필요합니다.` });
|
||||
const heights = [...new Set(ab.bearings.map((b) => b.bearingHeightMm))];
|
||||
if (heights.length > 1) issues.push({ code: 'AP07', level: 'W', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: 받침높이가 받침마다 다릅니다 (${heights.join(', ')} mm). 첫 값을 씁니다.` });
|
||||
ab.bearingHeightMm = heights[0] ?? NaN;
|
||||
}
|
||||
|
||||
return { abutments: abutments.filter((a) => Number.isFinite(a.ip1.e) && a.bearings.length >= 2), issues };
|
||||
}
|
||||
|
||||
/** The layout input of AA1–AA2 from a parsed 교대 record. */
|
||||
export function toAbutInput(rec: AbutRecord): AbutInput {
|
||||
return {
|
||||
name: rec.name,
|
||||
type: rec.type,
|
||||
seatEl: rec.seatEl,
|
||||
ip1: rec.ip1,
|
||||
ip2: rec.ip2,
|
||||
ip3: rec.ip3,
|
||||
bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })),
|
||||
skewDeg: rec.skewDeg,
|
||||
slopeI: rec.slopeI,
|
||||
wings: rec.wings,
|
||||
foundationBottomEl: rec.foundationBottomEl,
|
||||
pileLengthMm: rec.pileLengthMm,
|
||||
foundationType: rec.foundation,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 교대 형상 입력 (2026-11-03, 시안 ② 벽체·흉벽 · 확대기초·말뚝 카드): the values that used to be constants, in mm.
|
||||
* The defaults are the spec_abut values. A record keeps only the values the user changed; the rest come from here.
|
||||
*
|
||||
* Axis frame: t (교대축 직각) is + toward the front. 벽체 전면 = wallFrontMm; 배면 = wallFrontMm − wallThicknessMm.
|
||||
* 흉벽 sits behind the axis (t −parapetThicknessMm … 0). 저판: footingFrontMm (앞굽 끝) … footingBackMm (뒷굽 끝).
|
||||
*/
|
||||
export interface AbutShape {
|
||||
wallFrontMm: number;
|
||||
wallThicknessMm: number;
|
||||
parapetHeightMm: number;
|
||||
parapetThicknessMm: number;
|
||||
bracketHeightMm: number;
|
||||
footingThicknessMm: number;
|
||||
footingFrontMm: number;
|
||||
footingBackMm: number;
|
||||
pileEdgeMm: number;
|
||||
pileSpacingMm: number;
|
||||
}
|
||||
|
||||
export const ABUT_SHAPE_DEFAULT: AbutShape = {
|
||||
wallFrontMm: 1600,
|
||||
wallThicknessMm: 2400,
|
||||
parapetHeightMm: 2500,
|
||||
parapetThicknessMm: 500,
|
||||
bracketHeightMm: 850,
|
||||
footingThicknessMm: 1100,
|
||||
footingFrontMm: 2800,
|
||||
footingBackMm: -1800,
|
||||
pileEdgeMm: 650,
|
||||
pileSpacingMm: 2000,
|
||||
};
|
||||
|
||||
/** The same values in m, for the geometry (the model works in m). */
|
||||
export interface ResolvedShape {
|
||||
wallFrontM: number;
|
||||
wallBackM: number;
|
||||
parapetHeightM: number;
|
||||
parapetBackM: number;
|
||||
bracketHeightM: number;
|
||||
footingThicknessM: number;
|
||||
footingFrontM: number;
|
||||
footingBackM: number;
|
||||
pileEdgeM: number;
|
||||
pileSpacingM: number;
|
||||
pileRowsT: number[];
|
||||
}
|
||||
|
||||
/** The pile rows (t, m) of the spec: 배면 열, 중간 열, 전면 열. Not an input on the card. */
|
||||
export const PILE_ROWS_T = [-1.15, 0.85, 2.15];
|
||||
|
||||
/** Merges the user's changes over the defaults and converts to m. */
|
||||
export function resolveShape(changes?: Partial<AbutShape>): ResolvedShape {
|
||||
const v: AbutShape = { ...ABUT_SHAPE_DEFAULT, ...(changes ?? {}) };
|
||||
const wallBackMm = v.wallFrontMm - v.wallThicknessMm;
|
||||
return {
|
||||
wallFrontM: v.wallFrontMm / 1000,
|
||||
wallBackM: wallBackMm / 1000,
|
||||
parapetHeightM: v.parapetHeightMm / 1000,
|
||||
parapetBackM: -v.parapetThicknessMm / 1000,
|
||||
bracketHeightM: v.bracketHeightMm / 1000,
|
||||
footingThicknessM: v.footingThicknessMm / 1000,
|
||||
footingFrontM: v.footingFrontMm / 1000,
|
||||
footingBackM: v.footingBackMm / 1000,
|
||||
pileEdgeM: v.pileEdgeMm / 1000,
|
||||
pileSpacingM: v.pileSpacingMm / 1000,
|
||||
pileRowsT: PILE_ROWS_T,
|
||||
};
|
||||
}
|
||||
|
||||
/** Input problems in Korean; empty when the shape can be built. */
|
||||
export function shapeIssues(changes: Partial<AbutShape>): string[] {
|
||||
const v: AbutShape = { ...ABUT_SHAPE_DEFAULT, ...changes };
|
||||
const out: string[] = [];
|
||||
if (!(v.wallThicknessMm > 0)) out.push('벽체 두께는 0보다 커야 합니다.');
|
||||
if (!(v.parapetHeightMm > 0)) out.push('흉벽 높이는 0보다 커야 합니다.');
|
||||
if (!(v.parapetThicknessMm > 0)) out.push('흉벽 두께는 0보다 커야 합니다.');
|
||||
if (!(v.bracketHeightMm >= 0)) out.push('브라켓 높이는 0 이상이어야 합니다.');
|
||||
if (!(v.footingThicknessMm > 0)) out.push('저판 두께는 0보다 커야 합니다.');
|
||||
if (!(v.pileEdgeMm > 0) || !(v.pileSpacingMm > 0)) out.push('말뚝 연단과 간격은 0보다 커야 합니다.');
|
||||
const wallBack = v.wallFrontMm - v.wallThicknessMm;
|
||||
if (!(v.footingFrontMm > v.wallFrontMm)) out.push('저판 앞굽 끝은 벽체 전면보다 앞에 있어야 합니다.');
|
||||
if (!(v.footingBackMm < wallBack)) out.push('저판 뒷굽 끝은 벽체 배면보다 뒤에 있어야 합니다.');
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 교대 상세 (2026-11-03, 교각 상세와 같은 개념): the values the 삽도 shows, computed from the model. Read only.
|
||||
* Each row is [label, value]. Lengths in mm, levels in m, as in the 삽도.
|
||||
*/
|
||||
import type { AbutRecord } from './parse';
|
||||
import { buildAbut, type AbutModel } from './geometry';
|
||||
import { toAbutInput } from './parse';
|
||||
import { buildMembers } from './members';
|
||||
import { buildWings } from './wings';
|
||||
|
||||
const m3 = (v: number) => `${v.toFixed(3)} m`;
|
||||
const mm = (v: number) => `${Math.round(v)} mm`;
|
||||
|
||||
export function abutSummaryRows(rec: AbutRecord): [string, string][] {
|
||||
const model: AbutModel = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
const wings = buildWings(model, members, rec.wings);
|
||||
const shape = members.shape;
|
||||
const first = model.bearings[0];
|
||||
const L = model.length;
|
||||
const rows: [string, string][] = [
|
||||
['형식 / 기초 형식', `${rec.type} · ${rec.foundation}`],
|
||||
['교좌면 EL (구간 ①)', m3(rec.seatEl)],
|
||||
['기초 하단 EL', m3(rec.foundationBottomEl)],
|
||||
['지반고 EL', m3(rec.groundEl)],
|
||||
['저판 상면 EL', m3(members.footingTopEl)],
|
||||
['교대축 길이 (IP1 → IP3)', m3(L)],
|
||||
['skew', `${rec.skewDeg.toFixed(2)}°`],
|
||||
['종단경사 i', `${(rec.slopeI * 100).toFixed(2)} %`],
|
||||
['받침블럭 높이 (패드 포함)', mm(first.blockHeightMm)],
|
||||
['벽체 높이 (구간 ①)', m3(members.wallSegments.find((w) => Math.abs(w.seatEl - first.seatEl) < 1e-9)?.heightM ?? NaN)],
|
||||
['벽체 두께 · 전면', `${Math.round(shape.wallFrontM * 1000 - shape.wallBackM * 1000)} mm · ${Math.round(shape.wallFrontM * 1000)} mm`],
|
||||
['흉벽 높이 · 두께', `${Math.round(shape.parapetHeightM * 1000)} mm · ${Math.round(-shape.parapetBackM * 1000)} mm`],
|
||||
['흉벽 상단 EL (IP1 끝 / IP3 끝)', `${members.parapetTopStartEl.toFixed(3)} / ${members.parapetTopEndEl.toFixed(3)} m`],
|
||||
['브라켓 상면 EL (구간 ①)', m3(first.seatEl + shape.parapetHeightM - shape.bracketHeightM)],
|
||||
['흉벽 경사', `${(members.parapetSlope * 100).toFixed(3)} %`],
|
||||
['계단 경계 (IP3 쪽에서)', model.stepBounds.map((s) => (L - s).toFixed(3)).reverse().join(' / ') + ' m'],
|
||||
['저판 폭 (앞굽 끝 / 뒷굽 끝)', `${Math.round(shape.footingFrontM * 1000)} / ${Math.round(shape.footingBackM * 1000)} mm`],
|
||||
['말뚝 (열 × 본수)', `${members.piles.length} × ${members.piles[0]?.s.length ?? 0} = ${members.piles.length * (members.piles[0]?.s.length ?? 0)} 본`],
|
||||
['말뚝 선단 EL', members.pileBottomEl === undefined ? '—' : m3(members.pileBottomEl)],
|
||||
['수축줄눈 (ceil(L/10) − 1)', `${Math.max(0, Math.ceil(L / 10) - 1)} 개`],
|
||||
];
|
||||
if (wings.length > 0) {
|
||||
const w = wings[0];
|
||||
rows.push(['날개벽 경사', `${model.wing ? (model.wing.slopePct).toFixed(2) : '—'} %`]);
|
||||
rows.push(['날개벽 뒷굽 거리 (교대축 기준)', mm(w.heelDistanceM * 1000)]);
|
||||
}
|
||||
if (model.wing) rows.push(['접속슬래브 폭', mm(model.wing.slabWidthMm)]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 입력값 rows of the 교대 panel (시안 "A1 입력값" card): what the user typed, read only. */
|
||||
export function abutInputRows(rec: AbutRecord): [string, string][] {
|
||||
const rows: [string, string][] = [
|
||||
['형식 / 기초 형식', `${rec.type} (${rec.type === 'A1' ? '시점' : '종점'}) · ${rec.foundation}`],
|
||||
['교좌면 EL (구간 ①)', `${rec.seatEl.toFixed(3)} m`],
|
||||
['기초 하단 EL', `${rec.foundationBottomEl.toFixed(3)} m`],
|
||||
['지반고 EL', `${rec.groundEl.toFixed(3)} m`],
|
||||
['skew', `${rec.skewDeg.toFixed(2)}°`],
|
||||
['종단경사 i', `${(rec.slopeI * 100).toFixed(2)} %`],
|
||||
['말뚝 길이', `${Math.round(rec.pileLengthMm)} mm`],
|
||||
['IP', 'IP1 → IP3 직선'],
|
||||
['받침', `${rec.bearings.length}개 · z = 패드 상면`],
|
||||
];
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** The 8 detail rows shown in the collapsed 상세 (the rest stay in the 삽도 view). */
|
||||
export const ABUT_DETAIL_LABELS = [
|
||||
'벽체 높이 (구간 ①)',
|
||||
'흉벽 상단 EL (IP1 끝 / IP3 끝)',
|
||||
'브라켓 상면 EL (구간 ①)',
|
||||
'저판 상면 EL',
|
||||
'계단 경계 (IP3 쪽에서)',
|
||||
'말뚝 (열 × 본수)',
|
||||
'말뚝 선단 EL',
|
||||
'접속슬래브 폭',
|
||||
];
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 교대 날개벽 · AA3b (2026-11-03, spec_abut §4.7): 날개벽 평면, 상단선, 단부 하단. 순수 함수.
|
||||
*
|
||||
* - 평면: 교좌면 끝(IP1 끝 s = 0 또는 IP3 끝 s = L)에서 날개벽 선(AA2)을 따라 길이 6.5 m. 두께 0.45 m는 교대 안쪽으로 둔다.
|
||||
* - 상단: 시작점 = 흉벽 상단 EL(그 끝의 s), 끝까지 날개벽 경사(i)로 직선.
|
||||
* - 하단 (일반형): 단부 하단 = 끝점 상단 − 1.5 m. 단축형: 하단 = 저판 상면 높이(평평하게).
|
||||
* - 좌(①쪽) 날개벽은 받침 ①에 가까운 끝, 우(n쪽)는 반대 끝 (spec_abut §4.7).
|
||||
* - 하단 선 (AA3c): 저판 뒷굽 끝(배면 −1.8 m)까지는 저판 상면, 그 밖은 뒷굽 끝 상단 모서리에서 단부 하단까지 직선
|
||||
* (변화단면). 뒷굽 끝까지의 날개벽 거리 = 1.8 / |sin Δ| (단축형 1.807 m와 같은 식).
|
||||
* - 헌치 300 × 300 (AA3c): 흉벽 배면(−0.5) 모서리에 위 헌치, 벽체 배면(−0.8) 모서리에 아래 헌치. 수직 삼각기둥.
|
||||
* 위 헌치는 브라켓 상면(흉벽 상단 − 0.85)부터 날개벽 상단까지, 아래 헌치는 저판 상면부터 브라켓 상면까지다.
|
||||
* - 아직 없음: 등단면·변화단면 수평 단 구분 (QA-12), 저판 측면 윤곽의 정밀 모서리.
|
||||
*
|
||||
* 수치는 spec_abut §12 A2 합성 예제(좌 46.838 → 46.714, 하단 45.214 / 우 47.183 → 47.059, 하단 45.559)와 맞춘다.
|
||||
*/
|
||||
import type { AbutModel, AbutPoint, WingForm } from './geometry';
|
||||
import type { AbutMembers } from './members';
|
||||
|
||||
export const WING_LENGTH_M = 6.5;
|
||||
export const WING_THICKNESS_M = 0.45;
|
||||
/** 단부 높이 (끝점 상단에서 단부 하단까지, m). */
|
||||
export const WING_END_HEIGHT_M = 1.5;
|
||||
|
||||
export interface WingSolid {
|
||||
side: 'left' | 'right';
|
||||
form: Exclude<WingForm, 'none'>;
|
||||
/** 날개벽이 시작하는 축 끝: 0 (IP1 끝) 또는 L (IP3 끝). */
|
||||
sEnd: number;
|
||||
/** 평면 네 꼭짓점 (E, N), 순서: 시작, 끝, 끝 안쪽, 시작 안쪽. */
|
||||
corners: [AbutPoint, AbutPoint, AbutPoint, AbutPoint];
|
||||
topStartEl: number;
|
||||
topEndEl: number;
|
||||
/** 단부 하단 EL (일반형: 끝 상단 − 1.5; 단축형: 저판 상면). */
|
||||
bottomEndEl: number;
|
||||
/** 축 끝에서 날개벽 선을 따라 잰 뒷굽 끝까지의 거리 (m). */
|
||||
heelDistanceM: number;
|
||||
/** 상단선 (d = 축 끝에서 날개벽 방향 거리 m, EL m). */
|
||||
topLine: { dM: number; el: number }[];
|
||||
/** 하단선: 저판 상면 → 뒷굽 끝 → 단부 하단 (일반형), 또는 저판 상면 평탄 (단축형). */
|
||||
bottomLine: { dM: number; el: number }[];
|
||||
/** 헌치 300 × 300 (위·아래), 평면 삼각형과 높이 범위. */
|
||||
haunches: Haunch[];
|
||||
/** 수평 단 (QA-12, spec_abut §4.7): 등단면 / 변화단면1 / 변화단면2. 경계는 단부 높이 1.5 m와 교좌면 EL. */
|
||||
bands: WingBand[];
|
||||
}
|
||||
|
||||
export interface WingBand {
|
||||
name: '등단면' | '변화단면1' | '변화단면2' | '단축형 전구간';
|
||||
/** 높이 범위 (EL m). */
|
||||
zTopEl: number;
|
||||
zBottomEl: number;
|
||||
}
|
||||
|
||||
export interface Haunch {
|
||||
kind: 'upper' | 'lower';
|
||||
/** 평면 삼각형 꼭짓점 (E, N): 모서리, 날개벽 쪽 점, 벽체·흉벽 쪽 점. */
|
||||
corners: [AbutPoint, AbutPoint, AbutPoint];
|
||||
zBottomEl: number;
|
||||
zTopEl: number;
|
||||
}
|
||||
|
||||
export const HAUNCH_LEG_M = 0.3;
|
||||
|
||||
export function buildWings(model: AbutModel, members: AbutMembers, forms?: { left: WingForm; right: WingForm }): WingSolid[] {
|
||||
if (!model.wing) return [];
|
||||
const L = model.length;
|
||||
const wings = forms ?? model.wing.wings;
|
||||
// 좌 = 받침 ① 쪽 끝 (① 에 가까운 축 끝).
|
||||
const firstS = model.bearings[0].s;
|
||||
const leftS = firstS < L / 2 ? 0 : L;
|
||||
const rightS = leftS === 0 ? L : 0;
|
||||
const slope = model.wing.slopePct / 100;
|
||||
const dirRad = (model.wing.lineDeg * Math.PI) / 180;
|
||||
const w = { e: Math.cos(dirRad), n: Math.sin(dirRad) };
|
||||
const ip1 = model.ip1;
|
||||
// 축 끝에서 안쪽 방향: s = 0이면 +u, s = L이면 −u.
|
||||
const at = (s: number) => ({ e: ip1.e + model.u.e * s, n: ip1.n + model.u.n * s });
|
||||
|
||||
// 배면 쪽으로 가는 날개벽 방향의 횡방향 성분 k = |w · (전면 방향)|: 날개벽 거리 d는 횡방향 t = −d·k.
|
||||
const frontVec = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide };
|
||||
const k = Math.abs(w.e * frontVec.e + w.n * frontVec.n);
|
||||
const footingTop = members.footingTopEl;
|
||||
const heelDistanceM = Math.abs(members.shape.footingBackM) / k;
|
||||
|
||||
const build = (side: 'left' | 'right', sEnd: number, topStart: number, form: Exclude<WingForm, 'none'>): WingSolid => {
|
||||
const a = sEnd === 0 ? 1 : -1;
|
||||
const p0 = at(sEnd);
|
||||
const p1 = { e: p0.e + w.e * WING_LENGTH_M, n: p0.n + w.n * WING_LENGTH_M };
|
||||
const inward = { e: model.u.e * a * WING_THICKNESS_M, n: model.u.n * a * WING_THICKNESS_M };
|
||||
const topEnd = topStart + slope * WING_LENGTH_M;
|
||||
const bottomEnd = form === 'general' ? topEnd - WING_END_HEIGHT_M : footingTop;
|
||||
const topAt = (d: number) => topStart + slope * d;
|
||||
const topLine = [
|
||||
{ dM: 0, el: topStart },
|
||||
{ dM: WING_LENGTH_M, el: topEnd },
|
||||
];
|
||||
const bottomLine =
|
||||
form === 'general' && heelDistanceM < WING_LENGTH_M
|
||||
? [
|
||||
{ dM: 0, el: footingTop },
|
||||
{ dM: heelDistanceM, el: footingTop },
|
||||
{ dM: WING_LENGTH_M, el: bottomEnd },
|
||||
]
|
||||
: [
|
||||
{ dM: 0, el: footingTop },
|
||||
{ dM: WING_LENGTH_M, el: form === 'general' ? bottomEnd : footingTop },
|
||||
];
|
||||
|
||||
// 헌치: 위 헌치는 흉벽 배면(−0.5), 아래 헌치는 벽체 배면(−0.8)의 모서리에 둔다.
|
||||
const pointAt = (d: number) => ({ e: p0.e + w.e * d, n: p0.n + w.n * d });
|
||||
const shift = (q: AbutPoint, v: AbutPoint, f: number) => ({ e: q.e + v.e * f, n: q.n + v.n * f });
|
||||
const bracketTop = topStart - members.shape.bracketHeightM;
|
||||
const haunchAt = (kind: 'upper' | 'lower', t: number): Haunch => {
|
||||
const d = Math.abs(t) / k;
|
||||
const c = pointAt(d);
|
||||
const inwardUnit = { e: model.u.e * a, n: model.u.n * a };
|
||||
return {
|
||||
kind,
|
||||
corners: [c, shift(c, w, HAUNCH_LEG_M), shift(c, inwardUnit, HAUNCH_LEG_M)],
|
||||
zBottomEl: kind === 'upper' ? bracketTop : footingTop,
|
||||
zTopEl: kind === 'upper' ? topAt(d) : bracketTop,
|
||||
};
|
||||
};
|
||||
const haunches = [haunchAt('upper', members.shape.parapetBackM), haunchAt('lower', members.shape.wallBackM)];
|
||||
|
||||
// 수평 단 (QA-12): 등단면 = 끝 상단 − 1.5 m 위, 변화단면1 = 그 아래 ~ 교좌면 EL, 변화단면2 = 교좌면 EL ~ 저판 상면.
|
||||
// 교좌면 EL은 날개벽 끝에 가장 가까운 받침의 교좌면이다.
|
||||
const nearest = model.bearings.reduce((best, b) => (Math.abs(b.s - sEnd) < Math.abs(best.s - sEnd) ? b : best));
|
||||
const bands: WingBand[] =
|
||||
form === 'general'
|
||||
? [
|
||||
{ name: '등단면', zTopEl: topStart, zBottomEl: bottomEnd },
|
||||
{ name: '변화단면1', zTopEl: bottomEnd, zBottomEl: nearest.seatEl },
|
||||
{ name: '변화단면2', zTopEl: nearest.seatEl, zBottomEl: footingTop },
|
||||
]
|
||||
: [{ name: '단축형 전구간', zTopEl: topStart, zBottomEl: footingTop }];
|
||||
|
||||
return {
|
||||
side,
|
||||
form,
|
||||
sEnd,
|
||||
corners: [p0, p1, { e: p1.e + inward.e, n: p1.n + inward.n }, { e: p0.e + inward.e, n: p0.n + inward.n }],
|
||||
topStartEl: topStart,
|
||||
topEndEl: topEnd,
|
||||
bottomEndEl: bottomEnd,
|
||||
heelDistanceM,
|
||||
topLine,
|
||||
bottomLine,
|
||||
haunches,
|
||||
bands,
|
||||
};
|
||||
};
|
||||
|
||||
const out: WingSolid[] = [];
|
||||
if (wings.left !== 'none') out.push(build('left', leftS, leftS === 0 ? members.parapetTopStartEl : members.parapetTopEndEl, wings.left));
|
||||
if (wings.right !== 'none') out.push(build('right', rightS, rightS === 0 ? members.parapetTopStartEl : members.parapetTopEndEl, wings.right));
|
||||
return out;
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export interface ElevationRow {
|
||||
no: number;
|
||||
row: '전열' | '후열';
|
||||
elx: number;
|
||||
/** 교좌면 EL (m): the block bottom. */
|
||||
seatEl: number;
|
||||
h1: number;
|
||||
h2: number;
|
||||
h3: number;
|
||||
@@ -87,6 +89,7 @@ export function elevationRows(m: PierModel): ElevationRow[] {
|
||||
no: b.no,
|
||||
row: b.row,
|
||||
elx: b.padTop,
|
||||
seatEl: b.bottom,
|
||||
h1,
|
||||
h2,
|
||||
h3: plan.h3,
|
||||
@@ -125,3 +128,30 @@ export function solplateRows(m: PierModel): SolplateRow[] {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* S1·S2 of one pair along 교축 (mm, 2026-11-03): S1 = bearing centre to the coping edge on each outer side, S2 = between
|
||||
* the two bearing centres. The coping is centred on the axis with width B (model copingB), so S1 + S2 + S1 = B.
|
||||
*/
|
||||
export interface PairCopingSpans {
|
||||
S1Low: number;
|
||||
S2: number;
|
||||
S1High: number;
|
||||
copingWidth: number;
|
||||
}
|
||||
|
||||
export function pairCopingSpans(m: PierModel, pairIndex: number): PairCopingSpans | null {
|
||||
const pair = pairEccentricities(m.blocks)[pairIndex];
|
||||
if (!pair) return null;
|
||||
const f = m.blocks.find((b) => b.no === pair.frontNo && b.row === '전열')!;
|
||||
const k = m.blocks.find((b) => b.no === pair.backNo && b.row === '후열')!;
|
||||
const lo = Math.min(f.n, k.n);
|
||||
const hi = Math.max(f.n, k.n);
|
||||
const half = m.copingB / 2;
|
||||
return {
|
||||
S1Low: Math.round((lo + half) * 1000),
|
||||
S2: Math.round((hi - lo) * 1000),
|
||||
S1High: Math.round((half - hi) * 1000),
|
||||
copingWidth: Math.round(m.copingB * 1000),
|
||||
};
|
||||
}
|
||||
+10
-4
@@ -77,6 +77,8 @@ export interface BlockModel {
|
||||
rowSkewDeg: number;
|
||||
/** Solplate of this bearing (C안, 2026-11-03): the pier's rule, the same for every bearing. */
|
||||
plan: SolplatePlan;
|
||||
/** Block bottom from the 두부보 상면 slope (before any 교좌면 EL input), for the check against the input (2026-11-03). */
|
||||
seatCalc: number;
|
||||
}
|
||||
|
||||
/** Skew of a bearing row (deg) from the bearing azimuth and the stretch axis direction u (see {@link BlockModel.rowSkewDeg}). */
|
||||
@@ -435,13 +437,17 @@ export function buildPier(input: PierInput): PierModel {
|
||||
const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u);
|
||||
// Solplate of every bearing from the pier's rule (C안, 2026-11-03): the same plan for each bearing.
|
||||
const plan = solplatePlan(input.solplate ?? SOLPLATE_RULE_DEFAULT);
|
||||
// 교좌면 EL (2026-11-03): when the input gives it, the block bottom is that EL; otherwise the 두부보 상면 slope.
|
||||
const seatOf = new Map(input.bearings.map((r) => [r.no, r.seatEl]));
|
||||
frontPlaced.forEach((b) => {
|
||||
const bottom = segmentTop(segments, b.g);
|
||||
blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan });
|
||||
const seatCalc = segmentTop(segments, b.g);
|
||||
const bottom = seatOf.get(b.no) ?? seatCalc;
|
||||
blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan, seatCalc });
|
||||
});
|
||||
backPlaced.forEach((b) => {
|
||||
const bottom = b.z - h2;
|
||||
blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan });
|
||||
const seatCalc = b.z - h2;
|
||||
const bottom = seatOf.get(b.no) ?? seatCalc;
|
||||
blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan, seatCalc });
|
||||
});
|
||||
|
||||
const blockHeightFront = blocks.find((b) => b.row === '전열')!.top - blocks.find((b) => b.row === '전열')!.bottom;
|
||||
|
||||
@@ -186,5 +186,7 @@ function toBearing(c: Cell[]): Bearing {
|
||||
z: Number(c[4]),
|
||||
azimuthDeg: swapXYAzimuth(parseDms(String(c[5] ?? '')) ?? NaN),
|
||||
row: String(c[6]) as Row,
|
||||
// 교좌면_EL (m), optional: blank = not given, so the model keeps the 두부보 상면 slope.
|
||||
seatEl: c[7] === undefined || c[7] === null || c[7] === '' ? undefined : Number(c[7]),
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FckByMember, Material } from './rebar/common';
|
||||
import { checkStoredGroundModel, type GroundModelFile } from './ground-model';
|
||||
import type { PierInput } from './types';
|
||||
import type { AbutRecord } from './abut/parse';
|
||||
import { normalizeBearingAzimuths } from './coordinate-frame';
|
||||
|
||||
/** Project file schema (spec §8). Bump only with a migration. */
|
||||
@@ -37,6 +38,10 @@ export interface ProjectFile {
|
||||
fckByMember?: FckByMember;
|
||||
/** 지반 모델 (ground_model.json)의 교각별 지반고. Absent = 불러오지 않음. Applied to a pier only by the user. */
|
||||
groundModel?: GroundModelFile;
|
||||
/** 교대 (2026-11-03, AB): 교대 입력 기록. Absent in older files: no abutments. */
|
||||
abutments?: AbutRecord[];
|
||||
/** 교대 기본값 (가져온 그대로): 기본값 복원에 쓴다. Absent = 교대별 현재값이 기본값. */
|
||||
abutDefaults?: AbutRecord[];
|
||||
}
|
||||
|
||||
/** Serialises a project to the pretty-printed JSON stored in <교량명>_교각설계.pzr.json. */
|
||||
@@ -69,6 +74,8 @@ export function parseProject(text: string): ProjectFile {
|
||||
}
|
||||
}
|
||||
if (obj.groundModel !== undefined) checkStoredGroundModel(obj.groundModel);
|
||||
if (obj.abutments !== undefined) checkStoredAbutments(obj.abutments);
|
||||
if (obj.abutDefaults !== undefined) checkStoredAbutments(obj.abutDefaults);
|
||||
obj.piers.forEach((p, i) => {
|
||||
if (!p || typeof p.id !== 'string' || !p.input) throw new Error(`교각 ${i + 1}: id 또는 input이 없습니다.`);
|
||||
const input = p.input;
|
||||
@@ -99,3 +106,38 @@ export function parseProject(text: string): ProjectFile {
|
||||
});
|
||||
return obj as ProjectFile;
|
||||
}
|
||||
|
||||
const finite = (v: unknown) => typeof v === 'number' && Number.isFinite(v);
|
||||
const isPoint = (v: unknown) => !!v && typeof v === 'object' && finite((v as { e?: unknown }).e) && finite((v as { n?: unknown }).n);
|
||||
|
||||
/** 교대 기록 check (AB): refuses a damaged abutment record with a message that names it. */
|
||||
export function checkStoredAbutments(list: unknown): void {
|
||||
if (!Array.isArray(list)) throw new Error('교대 목록 형식이 맞지 않습니다.');
|
||||
list.forEach((a: Partial<AbutRecord>, i) => {
|
||||
const who = `교대 ${i + 1}(${String(a?.name)})`;
|
||||
if (!a || typeof a.name !== 'string' || !/^A\d+$/.test(a.name)) throw new Error(`${who}: 교대명이 맞지 않습니다.`);
|
||||
if (a.type !== 'A1' && a.type !== 'A2') throw new Error(`${who}: 형식은 A1 또는 A2여야 합니다.`);
|
||||
for (const key of ['seatEl', 'foundationBottomEl', 'groundEl', 'pileLengthMm', 'skewDeg', 'slopeI'] as const) {
|
||||
if (!finite(a[key])) throw new Error(`${who}: ${key} 값이 맞지 않습니다.`);
|
||||
}
|
||||
for (const key of ['ip1', 'ip2', 'ip3'] as const) {
|
||||
if (!isPoint(a[key])) throw new Error(`${who}: ${key} 좌표가 맞지 않습니다.`);
|
||||
}
|
||||
if (!Array.isArray(a.bearings) || a.bearings.length < 2) throw new Error(`${who}: 받침이 2개 이상 필요합니다.`);
|
||||
if (a.shape !== undefined) {
|
||||
for (const [key, v] of Object.entries(a.shape)) {
|
||||
if (!finite(v)) throw new Error(`${who}: 형상 값 ${key}이(가) 숫자가 아닙니다.`);
|
||||
}
|
||||
}
|
||||
if (a.wings !== undefined) {
|
||||
const forms = ['general', 'short', 'none'];
|
||||
if (!forms.includes(a.wings.left) || !forms.includes(a.wings.right)) throw new Error(`${who}: 날개벽 형식이 맞지 않습니다.`);
|
||||
}
|
||||
for (const b of a.bearings) {
|
||||
if (!b || !finite(b.no) || !isPoint(b.pos) || !finite(b.z) || (b.seatEl !== undefined && !finite(b.seatEl))) {
|
||||
throw new Error(`${who}: 받침 ${String(b?.no)}의 값이 맞지 않습니다.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function buildTemplateWorkbook(): XLSX.WorkBook {
|
||||
const bearing = [
|
||||
note('받침 1행당 받침 1개. 전열·후열 수는 같아야 합니다. 위치는 전열 또는 후열.'),
|
||||
note('z_m은 받침 EL(m), 방위각은 거더 방향입니다.'),
|
||||
['교각명', '받침번호', 'x_m', 'y_m', 'z_m', '방위각', '위치'],
|
||||
['교각명', '받침번호', 'x_m', 'y_m', 'z_m', '방위각', '위치', '교좌면_EL'],
|
||||
];
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(pier), '교각');
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface Bearing {
|
||||
z: number;
|
||||
azimuthDeg: number;
|
||||
row: Row;
|
||||
/** 교좌면 EL (m): the block bottom under this bearing. Absent = the 두부보 상면 slope (the coping top). */
|
||||
seatEl?: number;
|
||||
}
|
||||
|
||||
export interface PierInput {
|
||||
|
||||
@@ -38,6 +38,20 @@ export function validateInput(input: PierInput, issues: Issue[]): boolean {
|
||||
|
||||
/** Geometry-level checks (spec §10 W01–W05, W07, I01) on a built model. */
|
||||
export function validatePier(input: PierInput, model: PierModel, issues: Issue[]): void {
|
||||
// W10 (2026-11-03): the input 교좌면 EL should match the 두부보 상면 slope within 5 mm; the input wins.
|
||||
for (const b of model.blocks) {
|
||||
const seat = input.bearings.find((r) => r.no === b.no)?.seatEl;
|
||||
if (seat === undefined) continue;
|
||||
const diffMm = (seat - b.seatCalc) * 1000;
|
||||
if (Math.abs(diffMm) > 5) {
|
||||
issues.push({
|
||||
code: 'W10',
|
||||
level: 'W',
|
||||
pier: input.name,
|
||||
message: `${b.row} 받침 ${b.no}번 교좌면 EL ${seat.toFixed(3)}은(는) 두부보 상면 계산값 ${b.seatCalc.toFixed(3)}과 ${diffMm.toFixed(1)} mm 차이가 납니다 (허용 5 mm). 입력값을 씁니다.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const ip2Gap = Math.hypot(model.ip2Input.e - model.ip2Calc.e, model.ip2Input.n - model.ip2Calc.n);
|
||||
if (ip2Gap > 0.001) {
|
||||
issues.push({
|
||||
|
||||
Reference in new issue
Block a user