- 보호공(돌붙임) 띠 삭제, 윗면 선만 성토부선(계획선)으로 유지 — 구간별
사면길이·5m 초과 여부 툴팁 표기
- 성토부 5m 이상이면 조정창 +로 추가 기슭막이(배관 없는 동일 형상 벽)
계단식 추가 — 자동 자리는 배관용과 같은 규칙(1:1.2 최소 자리)
- 추가 벽도 ◀/▶/↺ 이동(안쪽 한계 = 앞 구조물), 마지막 벽 - 삭제
- 개수·이동량 세션 보관(b06:extrawall / revetx extra{n} 키)
- 700줄 제한: Extra(성토부선·추가 벽)·Wire(카드 배선) 분리 신설
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
203 lines
7.9 KiB
TypeScript
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 };
|
|
}
|