Files
Aislo/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts
T
eomsangdonandClaude Opus 5 9a58e3b1bd feat(B06): 유출부 보호공 삭제·성토부선 전환 + 추가 기슭막이(계단식)
- 보호공(돌붙임) 띠 삭제, 윗면 선만 성토부선(계획선)으로 유지 — 구간별
  사면길이·5m 초과 여부 툴팁 표기
- 성토부 5m 이상이면 조정창 +로 추가 기슭막이(배관 없는 동일 형상 벽)
  계단식 추가 — 자동 자리는 배관용과 같은 규칙(1:1.2 최소 자리)
- 추가 벽도 ◀/▶/↺ 이동(안쪽 한계 = 앞 구조물), 마지막 벽 - 삭제
- 개수·이동량 세션 보관(b06:extrawall / revetx extra{n} 키)
- 700줄 제한: Extra(성토부선·추가 벽)·Wire(카드 배선) 분리 신설

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 13:24:47 +09:00

203 lines
7.9 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Cross_Culvert_Extra.ts
* 유출측 **성토부선 + 추가 기슭막이**(2026-08-22 사용자) — Geom에서 분리(700줄 제한).
*
* 종전 보호공(돌붙임) 띠는 삭제하고 그 **윗면 선만 성토부선**으로 남긴다. 관 하단
* 꼭짓점에서 1:1.2로 원지반까지 내려가는 이 선이 5m 이상이면 기슭막이 의무 구간
* (성토_비탈면.md §2)이라 사용자가 **추가 기슭막이**(배관 없는 벽)를 둘 수 있다.
* 추가 벽 뒤에도 성토부가 5m 이상 남으면 또 추가할 수 있다(계단식).
* ========================================================================== */
import {
FILL_SLOPE_MAX_LENGTH_M,
REVET_EMBED_DEPTH_M,
REVET_LEAN_RATIO,
REVET_THICKNESS_M,
} from "./B06_Section_UI_Cross_Culvert_Const";
import {
clampWallOffset,
minShoulderWallOffset,
solveWallVertical,
} from "./B06_Section_UI_Cross_Culvert_Solve";
import type {
OffsetPoint,
OutletFillSegment,
WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Types";
export interface OutletExtrasInput {
/** 성토부선 시작점 = 관 유출 하단 꼭짓점. */
start: OffsetPoint;
outward: number;
groundAt: (offset: number) => number;
/** 성토부 물매(1:n) — 설계 성토 물매(기본 1.2). */
fillRatio: number;
/** 추가 벽 표준 높이(배관용과 동일: 관경+여유고, 0.1m 눈금). */
wallHeight: number;
/** 형태별 높이 한계(찰 3.0/메 2.0). */
heightLimit: number;
form: string | null;
/** 계류측 샘플 한계 offset. */
limitOffset: number;
/** 사용자가 민 이동량(m, + = 계류측 바깥) — 벽 개수만큼. */
shifts: number[];
}
export interface OutletExtrasResult {
walls: WallLayout[];
segments: OutletFillSegment[];
/** 한계에 잘린 뒤의 실제 이동량 — 조정창이 되받는다. */
appliedShifts: number[];
/** 끝 성토부가 아직 5m 이상 — 추가 기슭막이를 더 둘 수 있다. */
addable: boolean;
}
/**
* 성토부선(끝 구간) — src에서 1:n으로 내려가며 원지반을 만나면 끝난다.
* 지반이 사면보다 높은 구간은 지반을 따른다(종전 보호공 윗면과 동일 규칙,
* 최소 길이 연장은 보호공 삭제와 함께 없앴다).
*/
function trailingFill(
src: OffsetPoint,
outward: number,
ratio: number,
groundAt: (offset: number) => number,
): OutletFillSegment {
const points: OffsetPoint[] = [src];
let length = 0;
let previous = src;
const step = 0.25;
for (let t = step; t <= 30 + 1e-9; t += step) {
const offset = src.offset + outward * t;
const slopeElevation = src.elevation - t / ratio;
const groundElevation = groundAt(offset);
const point: OffsetPoint = { offset, elevation: Math.max(slopeElevation, groundElevation) };
length += Math.hypot(point.offset - previous.offset, point.elevation - previous.elevation);
points.push(point);
previous = point;
if (slopeElevation <= groundElevation) break;
}
return { points, lengthM: length, overLimit: length >= FILL_SLOPE_MAX_LENGTH_M - 1e-6 };
}
/** 성토부선이 원지반과 만나는 offset — 추가 벽 자동 자리 스캔의 사면 끝. */
function trailingToeOffset(segment: OutletFillSegment): number {
return segment.points[segment.points.length - 1].offset;
}
/**
* 추가 기슭막이 벽 1매 — 배관용 기슭막이와 **같은 형상**(배면 수직·전면 1:0.3,
* 수평 기초 근입 0.5m), 배관·마감면만 없다(2026-08-22 사용자 ③).
*/
function buildExtraWall(
index: number,
anchor: OffsetPoint,
outward: number,
height: number,
floatGapM: number,
form: string | null,
groundAt: (offset: number) => number,
): WallLayout {
const thickness = REVET_THICKNESS_M;
const baseWidth = thickness * 1.5 + REVET_LEAN_RATIO * height;
const backOffset = anchor.offset - outward * (baseWidth / 2);
const frontBase = anchor.offset + outward * (baseWidth / 2);
const topJoint = backOffset + outward * (thickness / 2);
const topElevation = anchor.elevation + height;
const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation };
const topFront = topJoint + outward * thickness;
const frontXAt = (elevation: number): number =>
topFront + outward * REVET_LEAN_RATIO * (topElevation - elevation);
let bottomElevation =
(floatGapM > 1e-6
? anchor.elevation
: Math.min(groundAt(backOffset), groundAt(frontBase), anchor.elevation)) -
REVET_EMBED_DEPTH_M;
if (floatGapM <= 1e-6) {
const toeGround = Math.min(groundAt(frontXAt(bottomElevation)), anchor.elevation);
bottomElevation = Math.min(bottomElevation, toeGround - REVET_EMBED_DEPTH_M);
}
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation };
const bottomFront: OffsetPoint = {
offset: frontXAt(bottomElevation),
elevation: bottomElevation,
};
return {
role: "extra",
extraIndex: index,
form,
lengthM: null,
backOffset,
outerOffset: bottomFront.offset,
base: anchor.elevation,
height,
floatGapM,
outward,
topBack,
topJoint: { offset: topJoint, elevation: topElevation },
bottomBack,
bottomFront,
points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront],
};
}
/**
* 유출측 성토부선·추가 기슭막이 일괄 계산.
*
* 벽 i의 자동 자리 = 앞 구간 시작점(src)을 노견 삼아 **1:1.2가 딱 성립하는 가장
* 안쪽 자리**(배관용 기슭막이와 같은 규칙 — minShoulderWallOffset 재사용). 사용자가
* 안쪽으로 당기면 높이 증가 → 한계 시 바닥 띄움(solveWallVertical 동일 사다리).
* 벽 상단 구간은 src → 이음선 상단점 직선, 벽 다음 src = 전면 상단 꼭짓점.
*/
export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult {
const { outward, groundAt, fillRatio, wallHeight, heightLimit, form, limitOffset } = input;
const walls: WallLayout[] = [];
const segments: OutletFillSegment[] = [];
const appliedShifts: number[] = [];
let src = input.start;
for (let i = 0; i < input.shifts.length; i += 1) {
const trailing = trailingFill(src, outward, fillRatio, groundAt);
// 성토부가 이미 원지반에 닿아 사실상 없으면 더 세울 수 없다 — 남은 벽은 버린다.
if (trailing.lengthM < 0.5) break;
const srcEdge = { offset_m: src.offset, elevation_m: src.elevation };
const toe = trailingToeOffset(trailing);
const auto =
minShoulderWallOffset(srcEdge, outward, wallHeight, groundAt, toe, limitOffset) ?? toe;
// 안쪽 당김 한계 = 구간 시작점(앞 구조물) — 그 안쪽은 앞 벽·관 몫이다.
const shifted = clampWallOffset(auto + outward * input.shifts[i], src.offset, outward);
appliedShifts.push(Math.round((shifted - auto) * outward * 10) / 10);
const vertical = solveWallVertical(
shifted,
srcEdge,
outward,
groundAt(shifted),
wallHeight,
heightLimit,
);
const wall = buildExtraWall(
i,
{ offset: shifted, elevation: vertical.baseElevation },
outward,
vertical.height,
vertical.floatGapM,
form,
groundAt,
);
walls.push(wall);
// src → 벽 이음선 상단점 직선 구간. 물매가 1:1.2보다 급하면 경고 대상(overLimit
// 아님 — 길이 기준만 적는다).
const run = Math.abs(wall.topJoint.offset - src.offset);
const rise = src.elevation - wall.topJoint.elevation;
segments.push({
points: [src, wall.topJoint],
lengthM: Math.hypot(run, rise),
overLimit: Math.hypot(run, rise) >= FILL_SLOPE_MAX_LENGTH_M - 1e-6,
});
// 다음 성토부 시작점 = 벽 전면 상단 꼭짓점(points[2]).
src = wall.points[2];
}
const tail = trailingFill(src, outward, fillRatio, groundAt);
segments.push(tail);
return { walls, segments, appliedShifts, addable: tail.overLimit };
}