B05에서 배치한 배수관 측점의 B06 횡단도에 배관·기슭막이(또는 집수정)·보호공을 분리 도형으로 그린다. 기존 도형(지반선·면적 밴드·포장·암 경계)은 손대지 않고, 계획선 표시 규칙만 세트와 정합을 맞춰 함께 정비했다. - 백엔드: pipe_points.json 정본(facility=="pipe")에 레지스트리 기본값을 채워 chainage ±0.02m로 맞는 횡단에 culvert 키를 얹는다(Engine_Culvert 신규, Router의 상세조회·재생성 두 경로에서 호출). 횡단 dict는 자유형이라 Pydantic 변경 없음. - 배관: 유입 invert = min(노견 아래 원지반, 노면고 − 관경 − 토피 0.5), 유출 앵커는 성토사면이 지반과 만나는 사면 끝(성토사면 길이 5m 한계 — 별표2 Ⅰ.2.차.(3)). 역경사는 수평 클램프, 길이는 m 단위 올림. 단면은 축 법선 방향 직사각형이고 외경선(실선)+내경선(점선) 2겹으로 관벽 두께를 보인다(파형강관 2.7T — KS D 3590). 끝단면은 구조물의 계류측 변을 복사해 자른다. - 기슭막이: 배면 수직 사다리꼴 + 전면 평행사변형 띠(1:0.3, t=0.45 — 실무 L3=45)의 합성 단면. 높이는 이음선 상단점이 성토 사면선과 만나는 접점까지, 하한 관경+0.45, 상한은 형태별 한계(찰 3.0/메 2.0 — 교본 7-3). 근입 0.5m(실무 기초콘크리트 H=0.5). 돌 해칭은 벽 폴리곤 clipPath 안에만 그려 어떤 높이에서도 벽 밖으로 새지 않는다. - 집수정: 절토측 유입이거나 원지반이 관 단면의 30% 이상을 막으면 자동 전환. I형(벽)·ㄴ형(+바닥)·ㄷ형(+반대측 막음), 기본 ㄴ형·내공 1.0m. 구조물이 원지반 안쪽에 박히면 절토선을 공사 계획선(보라 실선)으로 긋는다. 라벨은 I형 벽 하단. - 보호공: 시작 윗점 = 관 하단 꼭짓점, 시작 변은 기슭막이 전면과 같은 경사. 성토경사를 따라 원지반까지 내려간 뒤, 최소 길이(낙차고×2)를 채울 때까지 원지반을 따라 더 간다. - 계획선 표시: 원지반과 차이 없는 구간은 그리지 않되(보라 점선 폐지), 노면·노견· 측구부터 사면 1차 접점까지는 항상 그린다. 측구는 노견 바깥에 붙으므로 보호 범위를 측구 폭만큼 넓히고 접점 탐색도 그 바깥에서 시작한다. 배수관 측점은 벽 접점에서 트림. - B05: 배수관 측점 계획고를 지반고 + 관경 + 토피 0.5m 아래로 내리지 못하게 막고 경고 토스트를 띄운다. - 700줄 제한: 오버레이 파일이 919줄이 되어 기하(_Geom 678줄)와 그리기(247줄)로 분리했다. import 경로는 재수출로 유지해 B05·Cross_View는 건드리지 않았다. 근거 미확정(코드 주석·툴팁에 명시): 토피 0.5m(별표2 교량·암거 복토 50㎝ 교차 참조), 기슭막이 근입 0.5m(실무값), 흄관·VR관 관벽 두께(관경÷12 — KS F 4403 미확보). 검증: tsc, ruff, pytest 148 passed(신규 test_b06_culvert_set.py 6건), 헤드 브라우저 — 4+4.3·13+15.7 세트 표시, 보호공 시작점과 관 하단 꼭짓점 간격 0.6px 이내·시작 변 기울기 실측 0.223(= 0.3·H/(H+0.5) 이론값과 일치)·최소 길이 5.0m 확보, 계획선 세그먼트 연결 틈 0개(8·9·10·12·4+4.3). ⚠ 13+15.7 성토사면 5.03m로 법정 5m를 0.03m 초과 — 툴팁에 표기하고 접점 보간 여부는 사용자 판단 대기. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
730 lines
35 KiB
TypeScript
730 lines
35 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Cross_Culvert_Geom.ts
|
||
* 배수관 세트(배관·기슭막이·보호공) **기하 계산** — 그리기(`_Cross_Culvert.ts`)와 분리.
|
||
*
|
||
* 백엔드(`B06_Section_Engine_Culvert`)가 `pipe_points.json` 정본 + 레지스트리 기본값으로
|
||
* 만든 `section.culvert` 제원을 좌표로 옮기기만 한다 — 여기서 치수를 새로 정하지 않는다.
|
||
*
|
||
* 배치 규칙 — **실무 횡단도 4장 기준으로 재정립**(2026-08-20 사용자 제공, 울진 계열
|
||
* `관매설 Φ800~1000 · 기슭막이 유입 찰/유출 메 H=1.5 · 돌붙임(메붙임) ㎡` 주기):
|
||
* · 기슭막이 2기 — **도로면 양 가장자리**(유입 = 절토측 측구부, 유출 = 성토측 노견
|
||
* 바깥 = 성토사면 최상단)에 선다. 형상은 사면 쪽으로 **기운 평행사변형**(전면 1:0.3,
|
||
* 두께 0.45m), 상단 ≈ 노면고.
|
||
* · 배관 — 두 벽 사이(도로 하부)만 직선으로 지난다. 단면 항상 직사각형(축 법선 두께),
|
||
* 끝단면은 벽 전면과 평행하게 마감. 수평 가능. 길이 표기는 m 단위 올림.
|
||
* · 보호공(돌붙임) — **관 하단 꼭짓점**에서 시작해 성토사면 경사(1:n)를 따라 원지반까지,
|
||
* 최소 보호공 길이(낙차고×2)를 채울 때까지 지반을 따라 더 간다.
|
||
* · 유입측 — 수평 보호공 없음. 측구 접속은 설계선(표준단면 측구)이 이미 표현한다.
|
||
* · 성토 경사 — 벽 전면과 교차된 이후는 끊는다(designTrim — 벽·보호공이 대신한다).
|
||
*
|
||
* 유입 = 상단측(uphill_side), 유출 = 하단측. 유입구가 "집수정"이면 그쪽 기슭막이는
|
||
* 그리지 않고 집수정 단면(I/ㄴ/ㄷ형)으로 바꾼다.
|
||
* ========================================================================== */
|
||
|
||
import type {
|
||
CrossSection,
|
||
CulvertSet,
|
||
CulvertSideSpec,
|
||
SectionSample,
|
||
} from "./B06_Section_Api_Fetch";
|
||
|
||
/** 관 위 최소 토피(m) — 프론트 정의처(백엔드는 `B06_Section_Engine_Culvert.MIN_PIPE_COVER_M`).
|
||
* ⚠ 임도 배수관 토피 규정이 지식DB에 없어 별표2 교량·암거 "복토 50㎝ 이상"을 교차
|
||
* 참조로 채택(2026-08-19 사용자 확정). B05 배수관 측점 계획고 하향 차단 기준이자,
|
||
* 절토측 유입 벽에서 관 invert의 상한(노면 − 관경 − 토피)을 정한다. */
|
||
export const MIN_PIPE_COVER_M = 0.5;
|
||
|
||
/** 기슭막이 벽 두께(m) — 실무 견치돌 뒷길이 관측치 45㎝(울진 L3=45). 표시용 형상 값. */
|
||
export const REVET_THICKNESS_M = 0.45;
|
||
|
||
/** 기슭막이 전면 기울기(1:n) — 돌쌓기 전면 1:0.3(교본 7-3). 벽이 사면 쪽으로 기운다. */
|
||
export const REVET_LEAN_RATIO = 0.3;
|
||
|
||
/** 돌붙임 두께(m) — 실무 돌붙임 L3=45(울진 1공구 수량집계표). */
|
||
export const PITCHING_THICKNESS_M = 0.45;
|
||
|
||
/** 사다리꼴(배면부) 상단 폭(m) — 띠 두께의 절반(2026-08-20 사용자 확정). */
|
||
export const REVET_TRAP_TOP_M = REVET_THICKNESS_M / 2;
|
||
|
||
/**
|
||
* 관 벽 두께(m) — 관종별. 지식DB 조회(2026-08-20):
|
||
* · 파형강관: 강판 두께 **1.6T~3.2T**(교본 부록 4-6 §2.5.2, 표-1 표준형 강관 치수,
|
||
* KS D 3590). Ø1000 이상은 1.6~3.2 전 규격 사용 — 중간값 2.7T 채택.
|
||
* · 흄관·VR관(원심력 철근콘크리트관): 관경별 벽 두께 표가 지식DB에 없어
|
||
* 관경의 **1/12**을 적용한다(⚠ 근거 미확정 — KS F 4403 확보 시 교체).
|
||
* 도면상 내경·외경 2줄을 긋기 위한 값이다.
|
||
*/
|
||
export function pipeWallThicknessM(kind: string | null, diameterM: number): number {
|
||
if (kind && kind.includes("파형강관")) return 0.0027;
|
||
return Math.max(diameterM / 12, 0.05);
|
||
}
|
||
|
||
/**
|
||
* 기슭막이 근입 깊이(m) — 벽 바닥이 원지반 아래로 묻히는 깊이.
|
||
*
|
||
* 지식DB 조회 결과(2026-08-20):
|
||
* · 기슭막이 자체의 근입 깊이 **규정 없음** — 사방교본은 "기초 세굴 위험 시
|
||
* 밑다짐·바닥막이·수제 병행"만 정한다(`00_구조물_선정기준.md` §4).
|
||
* · 흙막이: "하부 땅밀림 방지 위해 **본바닥(원지반)에 기초**"(`흙막이.md`) —
|
||
* 원지반에 묻어야 한다는 원칙만 있고 수치는 없다.
|
||
* · 실무(울진소광 구조도): 돌기슭막이 **기초형 = 기초콘크리트 H=0.5**,
|
||
* 깬돌 ℓ3=45·1:0.3 (`_원단위라이브러리_울진소광.md` §4).
|
||
* → 실무 기초콘크리트 높이 0.5m를 근입 깊이로 채택한다. 임도 법정 확정값이 아니라
|
||
* **실무 관측 기반**이므로 확정 기준 확보 시 교체한다.
|
||
*/
|
||
export const REVET_EMBED_DEPTH_M = 0.5;
|
||
|
||
/**
|
||
* 기슭막이 형태별 높이 한계(m) — 지식DB `돌쌓기.md` §1 / `흙막이.md` §2:
|
||
* 돌 **찰쌓기 3.0m 이하**, 돌 **메쌓기 2.0m 이하**(교본 7-3 돌흙막이 기준),
|
||
* 돌망태 2m 정도. 큰돌쌓기는 총높이 한계 없음(1일 시공 1.5m 제한만).
|
||
* 초과 시 콘크리트 옹벽 등 상위 구조물 검토 대상이다.
|
||
*/
|
||
export function revetHeightLimit(form: string | null | undefined): number {
|
||
if (!form) return 3.0;
|
||
if (form.includes("메")) return 2.0;
|
||
if (form.includes("찰")) return 3.0;
|
||
if (form.includes("돌망태")) return 2.0;
|
||
if (form.includes("콘크리트")) return 4.0; // 콘크리트 흙막이 산복기초 4.0m 이하
|
||
return 3.0;
|
||
}
|
||
|
||
export interface OffsetPoint {
|
||
offset: number;
|
||
elevation: number;
|
||
}
|
||
|
||
/** 설계선(성토 경사) 트림 범위 — 이 밖은 벽·보호공이 대신하므로 그리지 않는다. */
|
||
export interface CulvertDesignTrim {
|
||
minOffset: number;
|
||
maxOffset: number;
|
||
}
|
||
|
||
export interface WallLayout {
|
||
role: "inlet" | "outlet";
|
||
form: string | null;
|
||
lengthM: number | null;
|
||
/** 배면(도로측) 바닥 offset. */
|
||
backOffset: number;
|
||
/** 전면(계류측) 바닥 offset. */
|
||
outerOffset: number;
|
||
base: number;
|
||
height: number;
|
||
outward: number;
|
||
/** 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점 — 데이터 좌표. */
|
||
points: OffsetPoint[];
|
||
/** 상단 배면 꼭짓점 — 성토 사면선과의 접점(설계선 트림 경계). */
|
||
topBack: OffsetPoint;
|
||
}
|
||
|
||
/**
|
||
* 유입 집수정 단면 — 실무 도면(2026-08-20 사용자 제공 L형 확대본) 기준.
|
||
* · **I형** = 기슭막이와 같은 **평행사변형 벽 1매**만.
|
||
* · **ㄴ(L)형** = I형 + **바닥판**.
|
||
* · **ㄷ형** = ㄴ형 + I형 **반대측 막음벽**.
|
||
* 돌 표현은 재질 확정 시 반영 예정(성토부 기슭막이와 동일 규칙) — 지금은 윤곽만.
|
||
*/
|
||
export type BasinShape = "I" | "L" | "U";
|
||
|
||
export interface BasinLayout {
|
||
shape: BasinShape;
|
||
/** 부재별 폴리곤(벽·바닥·막음벽). 각각 분리 도형으로 그린다. */
|
||
parts: Array<{ kind: "wall" | "floor"; points: OffsetPoint[] }>;
|
||
label: OffsetPoint;
|
||
/** 자동 전환 사유 — 툴팁·라벨에 쓴다. */
|
||
reason: "cut" | "blocked";
|
||
/** 집수정이 원지반 안으로 들어갈 때의 절토선(구조물 바깥 끝 → 지반 교차점). */
|
||
cutLine: { from: OffsetPoint; to: OffsetPoint } | null;
|
||
}
|
||
|
||
/** 관 끝단을 자를 마감면 — 구조물(기슭막이·집수정)의 계류측 변을 **그대로 복사**한다. */
|
||
export interface EndFace {
|
||
base: OffsetPoint;
|
||
direction: { offset: number; elevation: number };
|
||
}
|
||
|
||
/** 관 한쪽 끝단의 두 꼭짓점(마감면에서 자른 결과). */
|
||
export interface PipeEnd {
|
||
bottom: OffsetPoint;
|
||
top: OffsetPoint;
|
||
}
|
||
|
||
/** 유출측 보호공(돌붙임) 띠 — 윗면/밑면 폴리라인 쌍. */
|
||
export interface PitchingLayout {
|
||
top: OffsetPoint[];
|
||
base: OffsetPoint[];
|
||
lengthM: number;
|
||
/** 최소 보호공 길이(낙차고×2 — 백엔드 `apron_length_m`). */
|
||
minLengthM: number;
|
||
}
|
||
|
||
export interface CulvertLayout {
|
||
culvert: CulvertSet;
|
||
pipe: { inlet: OffsetPoint; outlet: OffsetPoint; lengthM: number; slopePct: number };
|
||
/** 관 축·법선 단위벡터(그리기가 두께 방향으로 밀 때 쓴다). */
|
||
pipeAxis: { axis: OffsetPoint; normal: OffsetPoint };
|
||
/** 관 양 끝단 꼭짓점 — 보호공 시작점이 유출 하단 꼭짓점을 그대로 쓴다. */
|
||
pipeCorners: { inlet: PipeEnd; outlet: PipeEnd };
|
||
walls: WallLayout[];
|
||
/** 유출측 성토사면 경사길이(m)와 법정 5m 충족 여부(별표2). */
|
||
fillSlope: { lengthM: number; withinLimit: boolean };
|
||
/** 유출측 보호공 띠(없으면 top/base 빈 배열). */
|
||
pitching: PitchingLayout;
|
||
/** 유입 집수정 단면(있을 때만). */
|
||
basin: BasinLayout | null;
|
||
designTrim: CulvertDesignTrim | null;
|
||
}
|
||
|
||
/** 유효 지반 샘플 → offset 오름차순 보간기. 범위 밖은 끝값 클램프, 샘플 없으면 null. */
|
||
function groundInterpolator(samples: SectionSample[]): ((offset: number) => number) | null {
|
||
const ground = samples
|
||
.filter((s) => s.valid !== false && s.elevation_m !== null && Number.isFinite(s.elevation_m))
|
||
.map((s) => ({ offset: s.offset_m ?? 0, elevation: s.elevation_m as number }))
|
||
.sort((a, b) => a.offset - b.offset);
|
||
if (!ground.length) return null;
|
||
return (offset: number): number => {
|
||
if (offset <= ground[0].offset) return ground[0].elevation;
|
||
const last = ground[ground.length - 1];
|
||
if (offset >= last.offset) return last.elevation;
|
||
for (let i = 1; i < ground.length; i += 1) {
|
||
if (offset > ground[i].offset) continue;
|
||
const a = ground[i - 1];
|
||
const b = ground[i];
|
||
const span = b.offset - a.offset;
|
||
if (span <= 0) return b.elevation;
|
||
return a.elevation + (b.elevation - a.elevation) * ((offset - a.offset) / span);
|
||
}
|
||
return last.elevation;
|
||
};
|
||
}
|
||
|
||
/** 설계선 보간기(offset 오름차순, 범위 밖 클램프). 설계선이 없으면 null. */
|
||
function designInterpolator(
|
||
line: Array<{ offset_m: number; elevation_m: number }> | undefined,
|
||
): ((offset: number) => number) | null {
|
||
if (!line || line.length < 2) return null;
|
||
const sorted = [...line].sort((a, b) => a.offset_m - b.offset_m);
|
||
return (offset: number): number => {
|
||
if (offset <= sorted[0].offset_m) return sorted[0].elevation_m;
|
||
const last = sorted[sorted.length - 1];
|
||
if (offset >= last.offset_m) return last.elevation_m;
|
||
for (let i = 1; i < sorted.length; i += 1) {
|
||
if (offset > sorted[i].offset_m) continue;
|
||
const a = sorted[i - 1];
|
||
const b = sorted[i];
|
||
const span = b.offset_m - a.offset_m;
|
||
if (span <= 0) return b.elevation_m;
|
||
return a.elevation_m + (b.elevation_m - a.elevation_m) * ((offset - a.offset_m) / span);
|
||
}
|
||
return last.elevation_m;
|
||
};
|
||
}
|
||
|
||
/** 두 직선(점 + 방향)의 교점. 거의 나란하면 null. */
|
||
function intersect(
|
||
p: OffsetPoint,
|
||
d: { offset: number; elevation: number },
|
||
q: OffsetPoint,
|
||
u: { offset: number; elevation: number },
|
||
): OffsetPoint | null {
|
||
const cross = u.offset * d.elevation - u.elevation * d.offset;
|
||
if (Math.abs(cross) < 1e-9) return null;
|
||
const t = ((p.offset - q.offset) * d.elevation - (p.elevation - q.elevation) * d.offset) / cross;
|
||
return { offset: q.offset + u.offset * t, elevation: q.elevation + u.elevation * t };
|
||
}
|
||
|
||
/** 성토사면 길이 한계(m) — **법정**: 별표2 Ⅰ.2.차.(3) "성토사면 길이 5m 이내, 초과 시
|
||
* 옹벽·석축 등 구조물 설치 의무"(지식DB 성토_비탈면.md §2). 유출 벽 자리의 바깥 한계. */
|
||
const FILL_SLOPE_MAX_LENGTH_M = 5.0;
|
||
|
||
/** 관 축과 벽 전면이 거의 나란하면 교점이 멀리 발산한다 — 이 거리를 넘으면 축 직각 마감. */
|
||
const STRAY_LIMIT_M = 2.5;
|
||
|
||
/** 노견에서 바깥으로 훑어 설계선(사면)이 지반과 만나는 첫 지점(사면 끝)을 찾는다.
|
||
* 사면 경사길이 5m 한계(별표2)에 걸리면 그 지점에서 멈춘다 — 거기가 구조물 자리다. */
|
||
function slopeToeOffset(
|
||
designAt: (offset: number) => number,
|
||
groundAt: (offset: number) => number,
|
||
startOffset: number,
|
||
limitOffset: number,
|
||
): number {
|
||
const stepCount = 120;
|
||
const step = (limitOffset - startOffset) / stepCount;
|
||
if (!Number.isFinite(step) || step === 0) return limitOffset;
|
||
let previousDiff = designAt(startOffset) - groundAt(startOffset);
|
||
let previousElevation = designAt(startOffset);
|
||
let slopeLength = 0;
|
||
for (let i = 1; i <= stepCount; i += 1) {
|
||
const offset = startOffset + step * i;
|
||
const elevation = designAt(offset);
|
||
slopeLength += Math.hypot(step, elevation - previousElevation);
|
||
previousElevation = elevation;
|
||
const diff = elevation - groundAt(offset);
|
||
if (previousDiff !== 0 && Math.sign(diff) !== Math.sign(previousDiff)) return offset;
|
||
if (Math.abs(diff) < 0.02) return offset;
|
||
if (slopeLength >= FILL_SLOPE_MAX_LENGTH_M) return offset;
|
||
previousDiff = diff;
|
||
}
|
||
return limitOffset;
|
||
}
|
||
|
||
/** 배수관 세트 기하 계산. 부족한 입력이면 null — 그리기와 분리(설계선 트림이 먼저 쓴다). */
|
||
export function computeCulvertLayout(
|
||
section: CrossSection,
|
||
groundSamples: SectionSample[],
|
||
): CulvertLayout | null {
|
||
const culvert = section.culvert;
|
||
if (!culvert) return null;
|
||
const groundAt = groundInterpolator(groundSamples);
|
||
if (!groundAt) return null;
|
||
const edges = section.design?.road_edges;
|
||
if (!edges) return null;
|
||
const designAt = designInterpolator(section.design?.design_line);
|
||
const sampleOffsets = groundSamples.map((sample) => sample.offset_m ?? 0);
|
||
const minSample = Math.min(...sampleOffsets);
|
||
const maxSample = Math.max(...sampleOffsets);
|
||
if (!(maxSample > minSample)) return null;
|
||
|
||
// 좌표 규약: +offset = 좌측(화면 왼쪽). 상단측이 유입이다(미상이면 좌측 폴백).
|
||
const uphill = section.uphill_side ?? "left";
|
||
const sideInfo = (side: "left" | "right") => ({
|
||
edge: side === "left" ? edges.left : edges.right,
|
||
outward: side === "left" ? 1 : -1,
|
||
limit: side === "left" ? maxSample : minSample,
|
||
});
|
||
const inletInfo = sideInfo(uphill === "left" ? "left" : "right");
|
||
const outletInfo = sideInfo(uphill === "left" ? "right" : "left");
|
||
|
||
// 유입 invert: 유입 노견 아래 원지반. 절토측이면 "노면 − 관경 − 토피"가 상한
|
||
// (관 상단 + 토피가 노면 안에 들어가는 최고 자리 — B05 하향 차단식과 동일).
|
||
const diameter = culvert.diameter_m;
|
||
const invertCap = (edge: { offset_m: number; elevation_m: number }): number =>
|
||
(designAt ? designAt(edge.offset_m) : edge.elevation_m) - diameter - MIN_PIPE_COVER_M;
|
||
const inletInvert = Math.min(groundAt(inletInfo.edge.offset_m), invertCap(inletInfo.edge));
|
||
const inlet: OffsetPoint = { offset: inletInfo.edge.offset_m, elevation: inletInvert };
|
||
|
||
// 유출 목표점 = 유출측 성토사면이 지반과 만나는 사면 끝(경사길이 5m 한계 — 별표2).
|
||
// 그 자리가 유출 기슭막이 자리이고, invert는 원지반(관 끝이 원지반 위 — 실무 도면).
|
||
const outletAnchorOffset = designAt
|
||
? slopeToeOffset(designAt, groundAt, outletInfo.edge.offset_m, outletInfo.limit)
|
||
: outletInfo.edge.offset_m;
|
||
// 성토사면 경사길이 실측(노견 → 사면 끝) — 법정 5m 이내 충족 확인용(사용자 ①).
|
||
let fillSlopeLength = 0;
|
||
if (designAt) {
|
||
const from = outletInfo.edge.offset_m;
|
||
const steps = 60;
|
||
let prevElev = designAt(from);
|
||
for (let i = 1; i <= steps; i += 1) {
|
||
const o = from + ((outletAnchorOffset - from) * i) / steps;
|
||
const e = designAt(o);
|
||
fillSlopeLength += Math.hypot((outletAnchorOffset - from) / steps, e - prevElev);
|
||
prevElev = e;
|
||
}
|
||
}
|
||
// 역경사는 수평으로 클램프(수평 가능 — 사용자 확정).
|
||
const outletInvert = Math.min(groundAt(outletAnchorOffset), inletInvert);
|
||
const outletAnchor: OffsetPoint = { offset: outletAnchorOffset, elevation: outletInvert };
|
||
|
||
const run0 = outletAnchor.offset - inlet.offset;
|
||
const rise0 = outletAnchor.elevation - inlet.elevation;
|
||
const length0 = Math.hypot(run0, rise0);
|
||
if (!(length0 > 0.5)) return null;
|
||
// 관은 m 단위 설치 — 올림 연장분은 유출 쪽으로(기슭막이를 조금 벗어나도 무관,
|
||
// 2026-08-20 사용자 확정). 실무 표기 L=10~12m대가 여기서 나온다.
|
||
let lengthM = Math.ceil(length0 - 1e-6);
|
||
const scale = lengthM / length0;
|
||
const outlet: OffsetPoint = {
|
||
offset: inlet.offset + run0 * scale,
|
||
elevation: inlet.elevation + rise0 * scale,
|
||
};
|
||
const span = Math.abs(outletAnchor.offset - inlet.offset);
|
||
const slopePct = span > 0 ? ((inletInvert - outletInvert) / span) * 100 : 0;
|
||
|
||
// 유입측 절토 판정: 절토측 유입은 자연스럽게 집수정(ㄴ형 기본)이 된다.
|
||
const mode = section.design?.section_mode;
|
||
const inletSideName: "left" | "right" = uphill === "left" ? "left" : "right";
|
||
const inletIsCut =
|
||
mode === "both_cut" ||
|
||
(mode === "left_cut" && inletSideName === "left") ||
|
||
(mode === "right_cut" && inletSideName === "right");
|
||
// 성토측 유입이라도 관 앞 원지반이 관 단면의 30% 이상을 막으면 집수정으로 바꾼다
|
||
// (2026-08-20 사용자 확정 — 자연 유입이 불가해 옆도랑 물을 받아야 하는 형상).
|
||
const inletFrontOffset =
|
||
inlet.offset + inletInfo.outward * (REVET_THICKNESS_M * 2 + REVET_LEAN_RATIO * diameter);
|
||
const blockedRatio = (groundAt(inletFrontOffset) - inletInvert) / Math.max(diameter, 1e-6);
|
||
const inletBlocked = !inletIsCut && blockedRatio >= 0.3;
|
||
const basinReason: BasinLayout["reason"] | null = inletIsCut
|
||
? "cut"
|
||
: inletBlocked
|
||
? "blocked"
|
||
: null;
|
||
|
||
// 기슭막이 합성 단면(사용자 ①·②): 하부 = 배면 수직·전면 1:0.3 **사다리꼴**(관이
|
||
// 지나는 관경 높이), 상부 = 전·배면이 나란히 기운 **평행사변형**. 상단 배면 꼭짓점이
|
||
// 성토 사면선(설계선)과 만나는 접점이 되도록 높이를 정한다 — 사면선은 거기서 끊긴다.
|
||
const walls: WallLayout[] = [];
|
||
let basin: BasinLayout | null = null;
|
||
/** 집수정이 서면 관 유입단을 내공 안으로 옮긴다(접속 표현). */
|
||
let basinPipeEnd: OffsetPoint | null = null;
|
||
/** 관 끝단 마감면 — 구조물 계류측 변 복사(2026-08-20 사용자 ③). */
|
||
const endFaces: { inlet: EndFace | null; outlet: EndFace | null } = {
|
||
inlet: null,
|
||
outlet: null,
|
||
};
|
||
let trimMin = Number.NEGATIVE_INFINITY;
|
||
let trimMax = Number.POSITIVE_INFINITY;
|
||
const buildWall = (
|
||
spec: CulvertSideSpec,
|
||
anchor: OffsetPoint,
|
||
outward: number,
|
||
forceBasinReason: BasinLayout["reason"] | null,
|
||
): WallLayout | null => {
|
||
const reason: BasinLayout["reason"] | null =
|
||
forceBasinReason ?? (spec.structure === "집수정" ? "cut" : null);
|
||
if (reason) {
|
||
// 집수정 = 기슭막이와 같은 **평행사변형 벽**(+ 형식에 따라 바닥·반대측 막음).
|
||
// 형식 기본값 = ㄴ(L)형(2026-08-20 사용자 확정). 실무 내공 1.0m(울진 돌집수정).
|
||
const shape: BasinShape = "L";
|
||
const innerWidth = 1.0;
|
||
const floorThickness = REVET_THICKNESS_M;
|
||
const roadTop = designAt ? designAt(anchor.offset) : anchor.elevation;
|
||
// 벽 높이: **상단 도로측 꼭지점이 성토 경사선(설계선)과 만나는 교점**까지
|
||
// (2026-08-20 사용자 — 기슭막이와 같은 접점 규칙). 하한은 관경 + 0.45.
|
||
const basinMinHeight = diameter + REVET_THICKNESS_M;
|
||
let wallHeight = Math.max(roadTop - anchor.elevation, basinMinHeight);
|
||
if (designAt) {
|
||
const wallBaseProbe = anchor.offset + outward * innerWidth;
|
||
for (let h = basinMinHeight; h <= 4.0 + 1e-6; h += 0.05) {
|
||
// 상단 도로측 꼭지점(기움 반전 — 상단이 도로측으로 물러난다).
|
||
const topInner = wallBaseProbe - outward * REVET_LEAN_RATIO * h;
|
||
wallHeight = h;
|
||
if (anchor.elevation + h >= designAt(topInner)) break;
|
||
}
|
||
wallHeight = Math.max(wallHeight, basinMinHeight);
|
||
}
|
||
// 벽 단면은 기슭막이와 동일(두께 0.45). 기움은 **수직 기준 반전** — 상단이
|
||
// 도로측(내공 쪽)으로 1:0.3 물러난다(2026-08-20 사용자 정정).
|
||
const wallOf = (baseOffset: number, dir: number): OffsetPoint[] => {
|
||
const topShift = -dir * REVET_LEAN_RATIO * wallHeight;
|
||
return [
|
||
{ offset: baseOffset, elevation: anchor.elevation - floorThickness },
|
||
{ offset: baseOffset + topShift, elevation: anchor.elevation + wallHeight },
|
||
{
|
||
offset: baseOffset + topShift + dir * REVET_THICKNESS_M,
|
||
elevation: anchor.elevation + wallHeight,
|
||
},
|
||
{
|
||
offset: baseOffset + dir * REVET_THICKNESS_M,
|
||
elevation: anchor.elevation - floorThickness,
|
||
},
|
||
];
|
||
};
|
||
// I형 벽 = 계류측(도로 바깥) 면 — 관 유입단에서 내공 1.0m 떨어져 선다.
|
||
const wallBase = anchor.offset + outward * innerWidth;
|
||
const parts: BasinLayout["parts"] = [{ kind: "wall", points: wallOf(wallBase, outward) }];
|
||
// ㄴ형 = I형 + 바닥판. 바닥은 **아래가 짧은 사다리꼴**(윗변이 길고 아랫변이
|
||
// 짧다 — 벽과 같은 1:0.3 물매가 양 끝에 붙는다). 위치는 **I형 벽 기준 반대측**
|
||
// = 도로측으로 뻗는다(2026-08-20 사용자 확정).
|
||
// 바닥은 **I형 벽의 바깥(계류측) 변에 좌측 변을 맞대고** 그 너머로 뻗는다
|
||
// (2026-08-20 사용자 확정). 벽이 기울어 있으므로 바닥 좌측 변도 **벽 바깥면 선을
|
||
// 그대로 따라** 기울여야 틈 없이 붙는다(높이마다 벽 x가 달라진다).
|
||
const wallOuterAt = (elevation: number): number => {
|
||
const bottom = anchor.elevation - floorThickness;
|
||
const t = (elevation - bottom) / (wallHeight + floorThickness);
|
||
return wallBase + outward * REVET_THICKNESS_M - outward * REVET_LEAN_RATIO * wallHeight * t;
|
||
};
|
||
const floorTopInner = wallOuterAt(anchor.elevation);
|
||
const floorBottomInner = wallOuterAt(anchor.elevation - floorThickness);
|
||
const floorOuter = floorTopInner + outward * (innerWidth + REVET_THICKNESS_M);
|
||
const floorTaper = REVET_LEAN_RATIO * floorThickness;
|
||
parts.push({
|
||
kind: "floor",
|
||
points: [
|
||
{ offset: floorTopInner, elevation: anchor.elevation },
|
||
{ offset: floorOuter, elevation: anchor.elevation },
|
||
{
|
||
offset: floorOuter - outward * floorTaper,
|
||
elevation: anchor.elevation - floorThickness,
|
||
},
|
||
{ offset: floorBottomInner, elevation: anchor.elevation - floorThickness },
|
||
],
|
||
});
|
||
// ㄷ형이면 반대측(도로측) 막음벽을 하나 더 세운다 — 지금은 L형 기본이라 미사용.
|
||
if ((shape as BasinShape) === "U") {
|
||
parts.push({ kind: "wall", points: wallOf(anchor.offset, -outward) });
|
||
}
|
||
// ㄴ·ㄷ형이 원지반 **안쪽**에 박히면 그만큼 절토가 필요하다(2026-08-20 사용자 ①).
|
||
// 구조물 바깥 끝 상단에서 표준단면 절토경사(1:n)로 원지반과 만나는 점까지 긋는다.
|
||
let basinCut: BasinLayout["cutLine"] = null;
|
||
if ((shape as BasinShape) !== "I") {
|
||
const outerTop = { offset: floorOuter, elevation: anchor.elevation };
|
||
if (groundAt(floorOuter) > anchor.elevation + 0.05) {
|
||
const cutRatio = section.design?.cut_slope_ratio ?? 1.0;
|
||
for (let h = 0.05; h <= 20; h += 0.05) {
|
||
const probe = floorOuter + outward * cutRatio * h;
|
||
if (groundAt(probe) <= anchor.elevation + h) {
|
||
basinCut = {
|
||
from: outerTop,
|
||
to: { offset: probe, elevation: anchor.elevation + h },
|
||
};
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
basin = {
|
||
shape,
|
||
parts,
|
||
// 라벨 자리 = **I형 벽 하단**(2026-08-20 사용자). 상단에 두면 노면·계획선과
|
||
// 겹친다 — 벽 바닥 변 중앙에 두고 그리기 쪽에서 아래로 내려 붙인다.
|
||
label: {
|
||
offset: wallBase + outward * (REVET_THICKNESS_M / 2),
|
||
elevation: anchor.elevation - floorThickness,
|
||
},
|
||
reason,
|
||
cutLine: basinCut,
|
||
};
|
||
// 성토 사면선은 **벽 상단 도로측 꼭지점(교점)**에서 끊는다 — 바닥 끝을 기준으로
|
||
// 두면 사면선이 집수정을 뚫고 들어간다(2026-08-20 사용자 ①).
|
||
const wallTopInner = wallBase - outward * REVET_LEAN_RATIO * wallHeight;
|
||
if (outward > 0) trimMax = Math.min(trimMax, wallTopInner);
|
||
else trimMin = Math.max(trimMin, wallTopInner);
|
||
// 관 유입 끝단 마감면 = 집수정 벽의 **계류측 변**을 그대로 복사(사용자 ③).
|
||
const faceBottom = {
|
||
offset: wallBase + outward * REVET_THICKNESS_M,
|
||
elevation: anchor.elevation - floorThickness,
|
||
};
|
||
const faceTop = {
|
||
offset: wallBase - outward * REVET_LEAN_RATIO * wallHeight + outward * REVET_THICKNESS_M,
|
||
elevation: anchor.elevation + wallHeight,
|
||
};
|
||
endFaces[spec.role] = {
|
||
base: faceBottom,
|
||
direction: {
|
||
offset: faceTop.offset - faceBottom.offset,
|
||
elevation: faceTop.elevation - faceBottom.elevation,
|
||
},
|
||
};
|
||
// 관은 집수정 내공을 가로질러 **안쪽 벽면까지** 물린다 — 벽 몸통 중간에서 끊기면
|
||
// 접속이 어긋나 보인다(2026-08-20 사용자 ③).
|
||
basinPipeEnd = {
|
||
offset: anchor.offset + outward * innerWidth,
|
||
elevation: anchor.elevation,
|
||
};
|
||
return null;
|
||
}
|
||
// 형상(사용자 스케치 확정 — 좌측 벽 기준, 우측은 반전): **배면(도로측) 수직**,
|
||
// **전면(계류측)은 1:0.3 기운 평행사변형 띠**. 띠 안쪽 평행선과 수직 배면 사이가
|
||
// 사다리꼴 — 합집합의 내부 대각 이음선은 배면 상단에서 바닥으로 내려온다.
|
||
// 바닥이 넓고 상단이 좁다(상단 폭 = 띠 두께 0.45).
|
||
// 높이: **이음선 상단점(평행사변형·사다리꼴 교차점)**이 성토 경사선과 교차하는
|
||
// 지점까지(2026-08-20 사용자 스케치 — 빨간 점). 하한은 관경 + 돌 한 단(0.45) —
|
||
// 관이 벽을 관통하므로 벽은 최소한 관을 덮어야 한다(사면 끝의 유출 벽은 접점
|
||
// 높이가 0이라 하한이 실제 높이가 된다). 옵션 높이는 상한.
|
||
const minHeight = diameter + REVET_THICKNESS_M;
|
||
const topJoint = anchor.offset + outward * REVET_TRAP_TOP_M;
|
||
let height = designAt
|
||
? Math.max(designAt(topJoint) - anchor.elevation, minHeight)
|
||
: (spec.revet_height_m ?? minHeight);
|
||
if (spec.revet_height_m != null) height = Math.min(height, spec.revet_height_m);
|
||
// 형태별 법정·교본 높이 한계(찰 3.0 / 메 2.0 — 돌쌓기.md §1)를 넘지 못한다.
|
||
height = Math.min(height, revetHeightLimit(spec.revet_form));
|
||
if (!(height > 0.05)) return null;
|
||
const topBack: OffsetPoint = {
|
||
offset: anchor.offset,
|
||
elevation: anchor.elevation + height,
|
||
};
|
||
// 상단 변: 사다리꼴 상단(0.45) + 띠 상단(0.45). 이음선은 상단 변 중간점(topJoint)
|
||
// 에서 1:0.3으로 바닥까지 — 사다리꼴(수직 배면, 상단 0.45 → 하단 0.45+0.3H)과
|
||
// 평행사변형 띠(폭 0.45)가 진짜 사다리꼴·평행사변형이 된다.
|
||
const topFront = topJoint + outward * REVET_THICKNESS_M;
|
||
const jointBase = topJoint + outward * REVET_LEAN_RATIO * height;
|
||
const frontBase = jointBase + outward * REVET_THICKNESS_M;
|
||
// 근입: 벽 바닥은 원지반 아래 REVET_EMBED_DEPTH_M만큼 묻힌다(흙막이 "본바닥에
|
||
// 기초" 원칙 + 실무 기초콘크리트 H=0.5). 바닥 꼭짓점 2개만 내리고 상단·이음선·
|
||
// 관 위치는 그대로 — 관은 여전히 원지반 높이로 지난다.
|
||
const embedBase = anchor.elevation - REVET_EMBED_DEPTH_M;
|
||
const wall: WallLayout = {
|
||
role: spec.role,
|
||
form: spec.revet_form ?? null,
|
||
lengthM: spec.revet_length_m ?? null,
|
||
backOffset: anchor.offset,
|
||
outerOffset: frontBase,
|
||
base: anchor.elevation,
|
||
height,
|
||
outward,
|
||
topBack,
|
||
points: [
|
||
{ offset: anchor.offset, elevation: embedBase },
|
||
topBack,
|
||
{ offset: topFront, elevation: anchor.elevation + height },
|
||
{ offset: frontBase, elevation: embedBase },
|
||
],
|
||
};
|
||
walls.push(wall);
|
||
// 성토 사면선은 **이음선 상단점**에서 끊는다(교차점 = 트림 경계 — 사용자 스케치).
|
||
if (outward > 0) trimMax = Math.min(trimMax, topJoint);
|
||
else trimMin = Math.max(trimMin, topJoint);
|
||
// 관 끝단 마감면 = 기슭막이의 **계류측 변**(전면)을 그대로 복사(사용자 ③).
|
||
endFaces[spec.role] = {
|
||
base: { offset: frontBase, elevation: embedBase },
|
||
direction: {
|
||
offset: topFront - frontBase,
|
||
elevation: anchor.elevation + height - embedBase,
|
||
},
|
||
};
|
||
return wall;
|
||
};
|
||
buildWall(culvert.inlet, inlet, inletInfo.outward, basinReason);
|
||
// 유출 벽 자리: 사면 끝에서는 사면선이 벽 상단보다 낮아 교차점이 성립하지 않는다.
|
||
// 관 축을 따라 노견 쪽으로 되짚으며 **이음선 상단점(base + 하한높이)이 사면선과
|
||
// 만나는 지점**을 찾는다 — 관·보호공 끝(anchor)은 그대로 두고 벽만 그 자리에 선다.
|
||
let outletWallAnchor = outletAnchor;
|
||
if (designAt) {
|
||
const axisRun = outletAnchor.offset - inlet.offset;
|
||
const axisRise = outletAnchor.elevation - inlet.elevation;
|
||
const invertAt = (offset: number): number =>
|
||
Math.abs(axisRun) > 1e-9
|
||
? inlet.elevation + axisRise * ((offset - inlet.offset) / axisRun)
|
||
: inlet.elevation;
|
||
const minWallHeight = diameter + REVET_THICKNESS_M;
|
||
const start = outletInfo.edge.offset_m;
|
||
const steps = 80;
|
||
for (let i = 0; i <= steps; i += 1) {
|
||
const offset = start + ((outletAnchor.offset - start) * i) / steps;
|
||
const invert = invertAt(offset);
|
||
const jointOffset = offset + outletInfo.outward * REVET_TRAP_TOP_M;
|
||
// 사면선고 − invert가 하한 높이까지 내려온 첫 지점 = 교차 성립 자리.
|
||
if (designAt(jointOffset) - invert <= minWallHeight + 1e-6) {
|
||
outletWallAnchor = { offset, elevation: invert };
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
const outletWall = buildWall(culvert.outlet, outletWallAnchor, outletInfo.outward, null);
|
||
// 유출 벽이 사면 끝보다 안쪽으로 당겨졌으면 **관 끝도 그 자리에 맞춘다** — 관만 사면
|
||
// 끝까지 남으면 벽과 떨어져 마감면 교차가 실패한다(2026-08-20 사용자 ② 원인).
|
||
if (outletWall && Math.abs(outletWallAnchor.offset - outletAnchor.offset) > 0.05) {
|
||
const runW = outletWallAnchor.offset - inlet.offset;
|
||
const riseW = outletWallAnchor.elevation - inlet.elevation;
|
||
const lenW = Math.hypot(runW, riseW);
|
||
if (lenW > 0.5) {
|
||
const scaleW = Math.ceil(lenW - 1e-6) / lenW;
|
||
outlet.offset = inlet.offset + runW * scaleW;
|
||
outlet.elevation = inlet.elevation + riseW * scaleW;
|
||
lengthM = Math.ceil(lenW - 1e-6);
|
||
}
|
||
}
|
||
|
||
// ── 관 단면 꼭짓점 — 끝단면을 구조물 계류측 변에 맞춰 자른다(2026-08-20 사용자 ③).
|
||
// 보호공 시작점이 **관 하단 꼭짓점**을 그대로 써야 하므로 여기서 확정한다.
|
||
const pipeStart = basinPipeEnd ?? inlet;
|
||
const runP = outlet.offset - pipeStart.offset;
|
||
const riseP = outlet.elevation - pipeStart.elevation;
|
||
const axisLength = Math.hypot(runP, riseP) || 1;
|
||
const axis: OffsetPoint = { offset: runP / axisLength, elevation: riseP / axisLength };
|
||
let normal: OffsetPoint = { offset: -axis.elevation, elevation: axis.offset };
|
||
if (normal.elevation < 0) normal = { offset: -normal.offset, elevation: -normal.elevation };
|
||
const topAnchor: OffsetPoint = {
|
||
offset: pipeStart.offset + normal.offset * diameter,
|
||
elevation: pipeStart.elevation + normal.elevation * diameter,
|
||
};
|
||
const endCorners = (role: "inlet" | "outlet", endPoint: OffsetPoint): PipeEnd => {
|
||
const fallback: PipeEnd = {
|
||
bottom: endPoint,
|
||
top: {
|
||
offset: endPoint.offset + normal.offset * diameter,
|
||
elevation: endPoint.elevation + normal.elevation * diameter,
|
||
},
|
||
};
|
||
const face = endFaces[role];
|
||
if (!face) return fallback;
|
||
const bottom = intersect(face.base, face.direction, pipeStart, axis);
|
||
const top = intersect(face.base, face.direction, topAnchor, axis);
|
||
if (!bottom || !top) return fallback;
|
||
const strayed =
|
||
Math.hypot(bottom.offset - endPoint.offset, bottom.elevation - endPoint.elevation) >
|
||
STRAY_LIMIT_M ||
|
||
Math.hypot(top.offset - fallback.top.offset, top.elevation - fallback.top.elevation) >
|
||
STRAY_LIMIT_M;
|
||
return strayed ? fallback : { bottom, top };
|
||
};
|
||
const pipeCorners = {
|
||
inlet: endCorners("inlet", pipeStart),
|
||
outlet: endCorners("outlet", outlet),
|
||
};
|
||
|
||
// 보호공(돌붙임) — 유출측. 2026-08-20 사용자 ①·②:
|
||
// ① 시작 변은 **기슭막이 전면과 같은 경사**(밑면을 벽 전면 방향으로 민다),
|
||
// 윗면 시작점 = **관 하단 꼭짓점**(관 밑면과 한 점에서 만난다).
|
||
// ② 성토사면 경사(1:n)로 원지반까지 내려간 뒤에도 **최소 보호공 길이**
|
||
// (낙차고×2 — 백엔드 `apron_length_m`)를 채울 때까지 원지반을 따라 더 간다.
|
||
// 끝단 윗점이 원지반 위에 놓인다.
|
||
const pitchingTop: OffsetPoint[] = [];
|
||
const pitchingBase: OffsetPoint[] = [];
|
||
let pitchingLength = 0;
|
||
const minPitchingM = culvert.outlet.apron_length_m ?? 0;
|
||
if (outletWall) {
|
||
const fillRatio = section.design?.fill_slope_ratio ?? 1.2;
|
||
// 밑면 이동 방향 = 기슭막이 전면 변과 나란한 **아래 방향** 단위벡터 × 두께.
|
||
let shift: OffsetPoint = { offset: 0, elevation: -PITCHING_THICKNESS_M };
|
||
const face = endFaces.outlet;
|
||
if (face) {
|
||
const faceLength = Math.hypot(face.direction.offset, face.direction.elevation);
|
||
if (faceLength > 1e-9) {
|
||
shift = {
|
||
offset: (-face.direction.offset / faceLength) * PITCHING_THICKNESS_M,
|
||
elevation: (-face.direction.elevation / faceLength) * PITCHING_THICKNESS_M,
|
||
};
|
||
}
|
||
}
|
||
const start = pipeCorners.outlet.bottom;
|
||
pitchingTop.push(start);
|
||
let previous = start;
|
||
const step = 0.25;
|
||
for (let t = step; t <= 30 + 1e-9; t += step) {
|
||
const offset = start.offset + outletWall.outward * t;
|
||
const slopeElevation = start.elevation - t / fillRatio;
|
||
const groundElevation = groundAt(offset);
|
||
const point: OffsetPoint = {
|
||
offset,
|
||
elevation: Math.max(slopeElevation, groundElevation),
|
||
};
|
||
pitchingLength += Math.hypot(
|
||
point.offset - previous.offset,
|
||
point.elevation - previous.elevation,
|
||
);
|
||
pitchingTop.push(point);
|
||
previous = point;
|
||
// 지반에 닿았고 최소 길이도 채웠으면 마감 — 아직이면 지반을 따라 계속 간다.
|
||
if (slopeElevation <= groundElevation && pitchingLength >= minPitchingM - 1e-6) break;
|
||
}
|
||
for (const point of pitchingTop) {
|
||
pitchingBase.push({
|
||
offset: point.offset + shift.offset,
|
||
elevation: point.elevation + shift.elevation,
|
||
});
|
||
}
|
||
}
|
||
|
||
return {
|
||
culvert,
|
||
pipe: { inlet: pipeStart, outlet, lengthM, slopePct },
|
||
pipeAxis: { axis, normal },
|
||
pipeCorners,
|
||
walls,
|
||
fillSlope: {
|
||
lengthM: fillSlopeLength,
|
||
withinLimit: fillSlopeLength <= FILL_SLOPE_MAX_LENGTH_M + 1e-6,
|
||
},
|
||
pitching: {
|
||
top: pitchingTop,
|
||
base: pitchingBase,
|
||
lengthM: pitchingLength,
|
||
minLengthM: minPitchingM,
|
||
},
|
||
basin,
|
||
designTrim: walls.length || basin ? { minOffset: trimMin, maxOffset: trimMax } : null,
|
||
};
|
||
}
|