feat(B06): 유출측 절토 복합경사 공용 도우미 — 흙막이 수평공간 뒤 · 박스 끝 · 세월교 측벽 끝 · ㄴ·ㄷ 집수정 끝 · 절토 쪽 배관 유출 벽 뒤 · 절토선 점열 · 3D(91-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -211,7 +211,7 @@ export function fordSilhouettes(section: CrossSection): StructureSilhouette[] {
|
||||
points.push(floorOuter);
|
||||
patchPoints.push(floorOuter);
|
||||
const run: OffsetPoint[] = [floorOuter];
|
||||
for (const cutPoint of [side.cutLine.from, side.cutLine.to]) {
|
||||
for (const cutPoint of side.cutLine) {
|
||||
const at = { offset_m: cutPoint.offset, elevation_m: cutPoint.elevation };
|
||||
points.push(at);
|
||||
patchPoints.push(at);
|
||||
@@ -347,8 +347,8 @@ export function culvertSilhouettes(section: CrossSection): StructureSilhouette[]
|
||||
}
|
||||
// 집수정이 원지반에 박히면 절토선까지 — 그만큼 원지반을 걷어낸다.
|
||||
if (layout.basin.cutLine) {
|
||||
push(inletSide, [layout.basin.cutLine.from, layout.basin.cutLine.to]);
|
||||
addRun(inletSide, [layout.basin.cutLine.from, layout.basin.cutLine.to]);
|
||||
push(inletSide, layout.basin.cutLine);
|
||||
addRun(inletSide, layout.basin.cutLine);
|
||||
basinCut = true;
|
||||
}
|
||||
spans[inletSide] = basinSpanOf(section);
|
||||
|
||||
@@ -136,7 +136,7 @@ export function appendBoxOverlay(
|
||||
}));
|
||||
if (side.cutLine)
|
||||
lines.push({
|
||||
points: [side.cutLine.from, side.cutLine.to],
|
||||
points: side.cutLine,
|
||||
text: `${mark}측 끝 절토선(계획선)`,
|
||||
});
|
||||
for (const line of lines) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
import { FILL_SLOPE_RATIO_MIN } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import { designInterpolator, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import { structureCutPoints, structureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
|
||||
|
||||
/** 구체 한쪽 끝의 조작값 — 좌·우를 따로 잡는다(2026-08-25 사용자). */
|
||||
export interface BoxSideAdjust {
|
||||
@@ -135,7 +136,8 @@ export function computeBoxLayout(
|
||||
// 본체는 늘 수평(79-7) — 좌 · 우 표고 조정이 달라도(옛 저장) 평균 하나로 섬.
|
||||
const riseM = (adjust.left.riseM + adjust.right.riseM) / 2;
|
||||
const sampleOffsets = groundSamples.map((sample) => sample.offset_m ?? 0);
|
||||
const cutSlopeRatio = section.design?.cut_slope_ratio ?? 1.0;
|
||||
// 끝 절토 = 본선 절토 사면과 같은 복합경사(91-4)
|
||||
const cutSpec = structureCutSpec(section.design);
|
||||
|
||||
const sideOf = (role: BoxSideRole): BoxSideLayout => {
|
||||
const outward = role === "left" ? 1 : -1;
|
||||
@@ -172,13 +174,7 @@ export function computeBoxLayout(
|
||||
let cutLine: BasinLayout["cutLine"] = null;
|
||||
let fillSegments: OutletFillSegment[] = [];
|
||||
if (groundAt(end.offset) > end.elevation + 0.05) {
|
||||
for (let h = 0.05; h <= 20; h += 0.05) {
|
||||
const probe = end.offset + outward * cutSlopeRatio * h;
|
||||
if (groundAt(probe) <= end.elevation + h) {
|
||||
cutLine = { from: end, to: { offset: probe, elevation: end.elevation + h } };
|
||||
break;
|
||||
}
|
||||
}
|
||||
cutLine = structureCutPoints(end, outward, cutSpec, groundAt);
|
||||
} else if (groundAt(end.offset) < end.elevation - 0.05) {
|
||||
fillSegments = buildExtrasAt(end, {
|
||||
startBottomElevation: bottomElevation,
|
||||
|
||||
@@ -225,11 +225,12 @@ export function appendCulvertOverlay(
|
||||
layer.append(shape);
|
||||
}
|
||||
if (basin.cutLine) {
|
||||
const cut = document.createElementNS(SVG_NS, "line");
|
||||
cut.setAttribute("x1", String(x(basin.cutLine.from.offset)));
|
||||
cut.setAttribute("y1", String(toDisplayY(basin.cutLine.from.elevation)));
|
||||
cut.setAttribute("x2", String(x(basin.cutLine.to.offset)));
|
||||
cut.setAttribute("y2", String(toDisplayY(basin.cutLine.to.elevation)));
|
||||
// 점열(암 무릎 · 소단 · 91-4)
|
||||
const cut = document.createElementNS(SVG_NS, "polyline");
|
||||
cut.setAttribute(
|
||||
"points",
|
||||
basin.cutLine.map((point) => `${x(point.offset)},${toDisplayY(point.elevation)}`).join(" "),
|
||||
);
|
||||
// 절토선은 **공사 계획선**이다 — 설계선과 같은 보라 실선으로 그린다
|
||||
// (2026-08-20 사용자 ②).
|
||||
cut.setAttribute("class", "b06-chart__design-cross");
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
REVET_FREEBOARD_M,
|
||||
REVET_LEAN_RATIO,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { CulvertSideSpec } from "./B06_Section_Api_Fetch";
|
||||
import type { CrossDesign, CulvertSideSpec } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
minShoulderWallOffset,
|
||||
slopeLengthAlong,
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
OffsetPoint,
|
||||
InletStructureChoice,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { structureCutPoints, structureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
|
||||
|
||||
/** 자동(기본) 집수정 모양(54-36) — 좌측 폼 「(기본)」 표시도 이 값. */
|
||||
export const DEFAULT_BASIN_SHAPE: BasinShape = "L";
|
||||
@@ -140,8 +141,10 @@ export interface BasinBuildInput {
|
||||
/** 이 측의 노견 끝 점 — 벽 상단 접점·I형 높이 성장 기준. */
|
||||
edge: { offset_m: number; elevation_m: number };
|
||||
groundAt: (offset: number) => number;
|
||||
/** 표준단면 절토 경사(1:n) — 절토 계획선용. */
|
||||
cutSlopeRatio: number;
|
||||
/** 표준단면 절토 경사(1:n) — 절토 계획선용(`cutDesign` 이 없을 때 경사 하나). */
|
||||
cutSlopeRatio?: number;
|
||||
/** 측점 설계값 — 있으면 절토선이 본선과 같은 복합경사(암 무릎 · 소단 · 91-4). */
|
||||
cutDesign?: Partial<CrossDesign> | null;
|
||||
adjust: BasinAdjust;
|
||||
/** 벽 두께(m). 기본 = 집수정 부재 두께. 세월교 측벽이 다른 두께를 쓸 때 넘긴다. */
|
||||
memberThicknessM?: number;
|
||||
@@ -183,17 +186,8 @@ export const BASIN_INNER_WIDTH_M = 1.0;
|
||||
|
||||
/** 집수정 단면을 만든다. 형태별 규칙은 파일 머리말 참조. */
|
||||
export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
const {
|
||||
anchor: baseAnchor,
|
||||
outward,
|
||||
shape,
|
||||
reason,
|
||||
diameterM,
|
||||
edge,
|
||||
groundAt,
|
||||
cutSlopeRatio,
|
||||
adjust,
|
||||
} = input;
|
||||
const { anchor: baseAnchor, outward, shape, reason, diameterM, edge, groundAt, adjust } = input;
|
||||
const cutSpec = structureCutSpec(input.cutDesign ?? { cut_slope_ratio: input.cutSlopeRatio });
|
||||
/** 이 부재의 기움 — 집수정 0.3, 세월교 0(수직). */
|
||||
const lean = input.leanRatio ?? REVET_LEAN_RATIO;
|
||||
const movedAnchor = {
|
||||
@@ -339,13 +333,7 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
: { offset: floorOuter, elevation: anchor.elevation };
|
||||
const groundHere = groundAt(refTop.offset);
|
||||
if (groundHere > refTop.elevation + 0.05) {
|
||||
for (let h = 0.05; h <= 20; h += 0.05) {
|
||||
const probe = refTop.offset + outward * cutSlopeRatio * h;
|
||||
if (groundAt(probe) <= refTop.elevation + h) {
|
||||
basinCut = { from: refTop, to: { offset: probe, elevation: refTop.elevation + h } };
|
||||
break;
|
||||
}
|
||||
}
|
||||
basinCut = structureCutPoints(refTop, outward, cutSpec, groundAt);
|
||||
} else if (groundHere < refTop.elevation - 0.05) {
|
||||
// 원지반 밖으로 나왔다 — 성토부선은 유출측과 같은 체계(1:1.2 + 5m 다단)로
|
||||
// 본체가 그린다(2026-08-22 사용자 확정). 여기서는 시작점만 넘긴다.
|
||||
|
||||
@@ -29,6 +29,8 @@ import type {
|
||||
WallAdjust,
|
||||
WallLayout,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { structureCutPoints } from "./B06_Section_UI_Cross_Structure_Cut";
|
||||
import type { StructureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
|
||||
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
|
||||
|
||||
export interface OutletExtrasInput {
|
||||
@@ -48,20 +50,19 @@ export interface OutletExtrasInput {
|
||||
equalize?: boolean;
|
||||
/** 단 벽의 전면 기울기 — 형태·순수 높이로 표준경사를 고름(없으면 종전 1:0.3). */
|
||||
leanFor?: (form: string, pureHeightM: number) => number;
|
||||
/** 끝이 원지반에 묻힐 때 수평공간 뒤 절토 경사(1:n)로 지반까지(세월교 · 박스 흙막이 · 79-7).
|
||||
* 없으면 종전 3도 선. */
|
||||
cutBerm?: { lengthM: number; slopeRatio: number };
|
||||
/** 끝이 원지반에 묻힐 때 수평공간 뒤 절토(본선과 같은 복합경사 · 91-4)로 지반까지(세월교 · 박스
|
||||
* 흙막이 · 79-7). 없으면 종전 3도 선. */
|
||||
cutBerm?: { lengthM: number; cut: StructureCutSpec };
|
||||
}
|
||||
|
||||
/** 수평공간(berm) 뒤 절토 경사로 원지반까지 — 공간 안에서 지반을 만나면 거기서 끝(79-7). */
|
||||
/** 수평공간(berm) 뒤 절토(복합경사 · 91-4)로 원지반까지 — 공간 안에서 지반을 만나면 거기서 끝(79-7). */
|
||||
function bermCut(
|
||||
src: OffsetPoint,
|
||||
outward: number,
|
||||
berm: { lengthM: number; slopeRatio: number },
|
||||
berm: { lengthM: number; cut: StructureCutSpec },
|
||||
groundAt: (offset: number) => number,
|
||||
limitOffset: number,
|
||||
): OutletFillSegment {
|
||||
const ratio = Math.max(berm.slopeRatio, 0.1);
|
||||
const flat = slopedCrossing(src, outward, 0, groundAt, limitOffset);
|
||||
const segment = (points: OffsetPoint[]): OutletFillSegment => ({
|
||||
points,
|
||||
@@ -78,22 +79,16 @@ function bermCut(
|
||||
if (flat !== null && (flat - src.offset) * outward <= berm.lengthM + 1e-9)
|
||||
return segment([src, { offset: flat, elevation: src.elevation }]);
|
||||
const bend = { offset: src.offset + outward * berm.lengthM, elevation: src.elevation };
|
||||
const span = (limitOffset - bend.offset) * outward;
|
||||
let previousGap = groundAt(bend.offset) - bend.elevation;
|
||||
for (let t = 0.25; t <= span + 1e-9; t += 0.25) {
|
||||
const gap = groundAt(bend.offset + outward * t) - (bend.elevation + t / ratio);
|
||||
if (gap <= 0) {
|
||||
const fraction = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
|
||||
const cross = t - 0.25 * (1 - fraction);
|
||||
return segment([
|
||||
src,
|
||||
bend,
|
||||
{ offset: bend.offset + outward * cross, elevation: bend.elevation + cross / ratio },
|
||||
]);
|
||||
}
|
||||
previousGap = gap;
|
||||
}
|
||||
return segment([src, bend]);
|
||||
// 소단 셈은 수평공간 끝에서 새로(91-4)
|
||||
const cut = structureCutPoints(
|
||||
bend,
|
||||
outward,
|
||||
berm.cut,
|
||||
groundAt,
|
||||
(limitOffset - bend.offset) * outward,
|
||||
);
|
||||
const head = berm.lengthM > 1e-9 ? [src, bend] : [src];
|
||||
return segment(cut ? [...head, ...cut.slice(1)] : head.length > 1 ? head : [src, bend]);
|
||||
}
|
||||
|
||||
export interface OutletExtrasResult {
|
||||
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
inletChoiceAvailability,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Basin";
|
||||
import { inletGroundConnector } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
import { withBackCut } from "./B06_Section_UI_Cross_Structure_Cut";
|
||||
import {
|
||||
buildCulvertTiers,
|
||||
drawnFillPoints,
|
||||
@@ -335,7 +336,7 @@ export function computeCulvertLayout(
|
||||
diameterM: diameter,
|
||||
edge: spec.role === "inlet" ? inletInfo.edge : outletInfo.edge,
|
||||
groundAt,
|
||||
cutSlopeRatio: section.design?.cut_slope_ratio ?? 1.0,
|
||||
cutDesign: section.design,
|
||||
adjust: basinAdjust,
|
||||
heightM: basinHeightM,
|
||||
});
|
||||
@@ -615,7 +616,7 @@ export function computeCulvertLayout(
|
||||
const slopeOf = (wall: WallLayout): FillSlopeSegment =>
|
||||
fillSlopeOf(wall, wall.role === "inlet" ? inletInfo.edge : outletInfo.edge);
|
||||
for (const wall of walls) {
|
||||
const slope = slopeOf(wall);
|
||||
const slope = withBackCut(slopeOf(wall), wall, section.design, groundAt, !culvert.hidden_pipe);
|
||||
// 사면선은 노견부터 우리가 그린다 — 트림 경계를 노견까지 당겨 백엔드 설계선의
|
||||
// 성토 구간을 통째로 덮는다(설계선은 1:1.2 고정이라 그대로 두면 다시 어긋난다).
|
||||
if (wall.outward > 0) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
import { fillWallBaseWidth } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import type { OffsetPoint, WallAdjust, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
|
||||
import { structureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
|
||||
|
||||
// d의 0점 = **성토선 길이 0 지점**(벽 상단 = 노견 — 2026-08-23 사용자 확정).
|
||||
// 그 자리(하단선 중점)는 이음선 상단점이 노견 끝에 오는 offset이고, 기본 자리는
|
||||
@@ -89,7 +90,7 @@ export function buildCulvertTiers(input: {
|
||||
culvert?.body
|
||||
? {
|
||||
lengthM: Math.max(spec?.revet_berm_m ?? GUARD_CUT_BERM_M, 0),
|
||||
slopeRatio: section.design?.cut_slope_ratio ?? 1.0,
|
||||
cut: structureCutSpec(section.design),
|
||||
}
|
||||
: undefined;
|
||||
// 성토부선 + 다단 기슭막이(2026-08-22 — 보호공 삭제, 윗면 선만 성토부선으로).
|
||||
|
||||
@@ -112,8 +112,8 @@ export interface BasinLayout {
|
||||
* "cut" = 절토측 유입(성토사면 없음), "short" = 성토사면 3m 이하,
|
||||
* "manual" = 규칙상 기슭막이 자리지만 사용자가 집수정을 선택. */
|
||||
reason: "cut" | "short" | "manual";
|
||||
/** 집수정이 원지반 안으로 들어갈 때의 절토선(구조물 바깥 끝 → 지반 교차점). */
|
||||
cutLine: { from: OffsetPoint; to: OffsetPoint } | null;
|
||||
/** 집수정이 원지반 안으로 들어갈 때의 절토선 점열(구조물 바깥 끝 → 복합경사 → 지반 교차점 · 91-4). */
|
||||
cutLine: OffsetPoint[] | null;
|
||||
/** 구조물이 원지반 밖으로 나올 때의 성토(되메움)선 — 경사 5도(임시), 지반 교차점까지. */
|
||||
fillLine: { from: OffsetPoint; to: OffsetPoint } | null;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export function appendFordOverlay(
|
||||
);
|
||||
}
|
||||
if (side.cutLine) {
|
||||
planLine([side.cutLine.from, side.cutLine.to], `${role} 측벽 절토선(계획선)`);
|
||||
planLine(side.cutLine, `${role} 측벽 절토선(계획선)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,6 @@ export function computeFordLayout(
|
||||
const uphill = section.uphill_side ?? "left";
|
||||
const inletOnLeft = uphill === "left";
|
||||
const roadTopAt = roadTopFactory(section, edges.left, edges.right);
|
||||
const cutSlopeRatio = section.design?.cut_slope_ratio ?? 1.0;
|
||||
|
||||
// 바닥판이 놓일 계류 하상 — 두 노견 사이 원지반 최저점.
|
||||
const bedElevation = (() => {
|
||||
@@ -182,7 +181,8 @@ export function computeFordLayout(
|
||||
diameterM: ford.diameter_m,
|
||||
edge: { offset_m: edge.offset_m, elevation_m: roadTopAt(edge.offset_m) },
|
||||
groundAt,
|
||||
cutSlopeRatio,
|
||||
// 측벽 끝 절토 = 본선 절토 사면과 같은 복합경사(91-4)
|
||||
cutDesign: section.design,
|
||||
// 내공 폭 자리에 날개벽 투영 연장을 넣는다 — 바닥판이 그만큼 계류측으로 뻗는다.
|
||||
adjust: {
|
||||
innerWidthM: Math.max(wing.slab_extend_m, 0),
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_Structure_Cut.ts
|
||||
* 구조물 끝 → 원지반 **절토선 점열**(91-4) — 본선 절토 사면과 같은 복합경사.
|
||||
*
|
||||
* 꼭짓점은 `common_util_cross_berm.cutProfilePoints` 그대로(암 → 토사 무릎 · 소단) · 경계 = 원지반 +
|
||||
* `rock_boundary_offset_m` · 켬 = 암 프리셋 + `two_stage_slope` · 소단 = `design.berm` · 소단 셈은
|
||||
* 구조물 끝에서 새로 · 원지반 교점에서 자름.
|
||||
* 쓰는 곳 = 흙막이 수평공간 뒤 · 박스 끝 · 세월교 측벽 끝 · ㄴ·ㄷ 집수정 끝 · 절토 쪽 배관 유출 벽 뒤.
|
||||
* ========================================================================== */
|
||||
|
||||
import { bermSpec, cutProfilePoints } from "../common_util/common_util_cross_berm";
|
||||
import type { BermSpec } from "../common_util/common_util_cross_berm";
|
||||
import type { CrossDesign } from "./B06_Section_Api_Fetch";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
/** 절토 제원 — 암(또는 단일) 경사 · 토사 경사 · 암 경계 오프셋(없으면 경사 하나) · 소단. */
|
||||
export interface StructureCutSpec {
|
||||
cutRatio: number;
|
||||
soilRatio: number;
|
||||
rockOffsetM: number | null;
|
||||
berm: BermSpec | null;
|
||||
}
|
||||
|
||||
/** 측점 설계값에서 절토 제원 — 본선 `computeCrossDesign` 과 같은 켬 조건. */
|
||||
export function structureCutSpec(design?: Partial<CrossDesign> | null): StructureCutSpec {
|
||||
const cutRatio = Math.max(design?.cut_slope_ratio ?? 1.0, 0.1);
|
||||
const offset = design?.rock_boundary_offset_m;
|
||||
const twoStage =
|
||||
design?.geometry_preset === "rock" && design.two_stage_slope === true && offset != null;
|
||||
const berm = design?.berm;
|
||||
return {
|
||||
cutRatio,
|
||||
soilRatio: Math.max(design?.soil_cut_slope_ratio ?? cutRatio, 0.1),
|
||||
rockOffsetM: twoStage ? offset : null,
|
||||
berm: berm ? bermSpec(berm.width_m, berm.interval_m, berm.slope_deg) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 원지반 교점 찾는 걸음(m). */
|
||||
const STEP_M = 0.05;
|
||||
|
||||
/**
|
||||
* `from`(원지반 아래) → 바깥(`outward`)으로 절토 꼭짓점 · 마지막 점 = 원지반 교점.
|
||||
* `reachM` 안에서 못 만나면 null.
|
||||
*/
|
||||
export function structureCutPoints(
|
||||
from: OffsetPoint,
|
||||
outward: number,
|
||||
spec: StructureCutSpec,
|
||||
groundAt: (offset: number) => number,
|
||||
reachM = 50,
|
||||
): OffsetPoint[] | null {
|
||||
const at = (dist: number): number => from.offset + outward * dist;
|
||||
const rockOffsetM = spec.rockOffsetM;
|
||||
const boundary = rockOffsetM === null ? null : (dist: number) => groundAt(at(dist)) + rockOffsetM;
|
||||
const profile = cutProfilePoints(
|
||||
0,
|
||||
from.elevation,
|
||||
spec.cutRatio,
|
||||
spec.soilRatio,
|
||||
boundary,
|
||||
spec.berm,
|
||||
Math.max(reachM, STEP_M),
|
||||
// 소단이 있으면 경계를 오갈 때마다 꺾음 — 본선 `cutPoints` 와 같음.
|
||||
spec.berm !== null,
|
||||
);
|
||||
const points: OffsetPoint[] = [from];
|
||||
for (let i = 1; i < profile.length; i += 1) {
|
||||
const [d0, z0] = profile[i - 1];
|
||||
const [d1, z1] = profile[i];
|
||||
const steps = Math.max(1, Math.ceil((d1 - d0) / STEP_M));
|
||||
let previousGap = groundAt(at(d0)) - z0;
|
||||
for (let k = 1; k <= steps; k += 1) {
|
||||
const t = k / steps;
|
||||
const gap = groundAt(at(d0 + (d1 - d0) * t)) - (z0 + (z1 - z0) * t);
|
||||
if (gap <= 0) {
|
||||
// 걸음 사이 교점 보간 — 선이 지반을 넘지 않게
|
||||
const share = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
|
||||
const tc = (k - 1 + share) / steps;
|
||||
points.push({ offset: at(d0 + (d1 - d0) * tc), elevation: z0 + (z1 - z0) * tc });
|
||||
return points;
|
||||
}
|
||||
previousGap = gap;
|
||||
}
|
||||
points.push({ offset: at(d1), elevation: z1 });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배관 유출 벽이 절토 쪽(벽 바깥 윗모서리가 원지반 아래)이면 그 모서리부터 절토선을 사면 점열 뒤에
|
||||
* 이음 — 트림이 노견부터 설계선을 지워 절토사면이 사라지던 자리(91-4). 관 숨김(세월교 · 박스 흙막이 ·
|
||||
* 독립 기슭막이)은 `enabled` 거짓.
|
||||
*/
|
||||
export function withBackCut<T extends { points: OffsetPoint[] }>(
|
||||
slope: T,
|
||||
wall: { role: string; outward: number; points: OffsetPoint[] },
|
||||
design: Partial<CrossDesign> | null | undefined,
|
||||
groundAt: (offset: number) => number,
|
||||
enabled: boolean,
|
||||
): T {
|
||||
const corner = wall.points[2];
|
||||
if (!enabled || wall.role !== "outlet" || !corner) return slope;
|
||||
if (groundAt(corner.offset) <= corner.elevation + 0.05) return slope;
|
||||
const cut = structureCutPoints(corner, wall.outward, structureCutSpec(design), groundAt);
|
||||
return cut ? { ...slope, points: [...slope.points, ...cut] } : slope;
|
||||
}
|
||||
@@ -127,4 +127,65 @@ out.lean = ["left_cut", "right_cut", "both_fill", "both_cut"].map((mode) => [
|
||||
leanOf({ form: "돌쌓기(찰)", height_m: 1.5, section_mode: mode, side: "좌" }),
|
||||
leanOf({ form: "돌쌓기(찰)", height_m: 1.5, section_mode: mode, side: "좌", face_role: "절토" }),
|
||||
]);
|
||||
// ③ 91-4 유출측 절토 복합경사 — 꼭짓점 사이 경사(수평/수직) · 사면 길이 · 끝 = 원지반 교점
|
||||
const { groundInterpolator } = loadTs(B06("B06_Section_UI_Cross_Culvert_Solve.ts"));
|
||||
function profile(points, samples) {
|
||||
const groundAt = groundInterpolator(samples);
|
||||
const legs = points.slice(1).map((p, i) => {
|
||||
const dx = Math.abs(p.offset - points[i].offset);
|
||||
const dz = p.elevation - points[i].elevation;
|
||||
return { ratio: dz > 1e-9 ? r3(dx / dz) : null, slant: r3(Math.hypot(dx, dz)), run: r3(dx) };
|
||||
});
|
||||
const end = points[points.length - 1];
|
||||
return { legs, endGap: r3(Math.abs(groundAt(end.offset) - end.elevation)) };
|
||||
}
|
||||
const withBerm = (s) => {
|
||||
s.design.berm = { width_m: 0.5, interval_m: 3, slope_deg: 0 };
|
||||
};
|
||||
// 흙막이 수평공간 뒤(박스 352.14 · 세월교 173.09)
|
||||
function guardTails(base, edit = () => undefined) {
|
||||
const s = clone(base);
|
||||
edit(s);
|
||||
const { culvert } = layoutsOf(s, base);
|
||||
return [...culvert.outletFill.segments, ...culvert.basinFill.segments]
|
||||
.filter((g) => g.kind === "cut")
|
||||
.map((g) => profile(g.points, s.samples));
|
||||
}
|
||||
out.guardTails = [...guardTails(at(352.14)), ...guardTails(FORD)];
|
||||
// 박스 끝 · 세월교 측벽 끝(흙막이 없음)
|
||||
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts"));
|
||||
const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford_Geom.ts"));
|
||||
function endCuts(base, compute) {
|
||||
const s = clone(base);
|
||||
delete s.culvert;
|
||||
s.uphill_side = "left"; // 절토 쪽(우측)을 유출로
|
||||
return compute(s, s.samples)
|
||||
.sides.filter((side) => side.cutLine)
|
||||
.map((side) => profile(side.cutLine, s.samples));
|
||||
}
|
||||
out.boxEnd = endCuts(at(352.14), computeBoxLayout);
|
||||
out.fordEnd = endCuts(FORD, computeFordLayout);
|
||||
// 절토 쪽 배관 유출 벽 뒤(관 340 · 유출 = 우측 절토) · 소단 있음/없음
|
||||
function cutOutlet(edit = () => undefined) {
|
||||
const s = clone(at(340));
|
||||
s.uphill_side = "left";
|
||||
s.culvert = {
|
||||
type: "pipe",
|
||||
chainage_m: 340,
|
||||
pipe_kind: "흄관",
|
||||
diameter_m: 1.0,
|
||||
min_cover_m: 0.5,
|
||||
inlet: { role: "inlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" },
|
||||
outlet: { role: "outlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" },
|
||||
};
|
||||
s.design.inlet_structure = "revet";
|
||||
edit(s);
|
||||
const layout = computeCulvertLayout(s, s.samples, undefined, "revet");
|
||||
const wall = layout.walls.find((w) => w.role === "outlet");
|
||||
const pts = layout.designTrim.minSlope.points;
|
||||
const from = pts.findIndex((p) => Math.abs(p.offset - wall.points[2].offset) < 1e-9);
|
||||
return profile(pts.slice(from), s.samples);
|
||||
}
|
||||
out.cutOutlet = cutOutlet();
|
||||
out.cutOutletBerm = cutOutlet(withBerm);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -80,7 +80,8 @@ def test_흙막이_끝_절토는_수평공간_뒤(shape: dict) -> None:
|
||||
tails = [tail for group in shape["cutDefault"] for tail in group]
|
||||
assert tails, "절토 끝선이 없음"
|
||||
for tail in tails:
|
||||
assert tail["points"] == 3
|
||||
# 시작 · 수평공간 끝 · (암 무릎 · 소단) · 원지반 교점 — 본선 절토와 같은 복합경사(91-4)
|
||||
assert tail["points"] >= 3
|
||||
assert tail["flat"] == pytest.approx(0, abs=1e-6)
|
||||
assert tail["berm"] == pytest.approx(1.0, abs=1e-3)
|
||||
for tail in shape["cutBerm2"]:
|
||||
|
||||
@@ -83,3 +83,34 @@ def test_지형에_막힘_잔재_없음() -> None:
|
||||
source = (ROOT / "B06_Section" / name).read_text(encoding="utf-8")
|
||||
assert "shiftBlockedM" not in source and "지형에 막힘" not in source, name
|
||||
assert "noticeExtraLimit" not in source and "noticeShortfall" not in source, name
|
||||
|
||||
|
||||
def _knee(cut: dict) -> bool:
|
||||
"""암(1:0.4) 뒤 토사(1:1.0) 무릎이 있음 — 픽스처 = 리핑암 · 2단계 · 경계 −0.5 m."""
|
||||
ratios = [leg["ratio"] for leg in cut["legs"] if leg["ratio"] is not None]
|
||||
return any(a == 0.4 and b == 1.0 for a, b in zip(ratios, ratios[1:]))
|
||||
|
||||
|
||||
def test_흙막이_수평공간_뒤_절토_복합경사(fit: dict) -> None:
|
||||
tails = fit["guardTails"]
|
||||
assert tails and all(cut["endGap"] < 0.01 for cut in tails)
|
||||
assert all(cut["legs"][0]["ratio"] is None for cut in tails) # 수평공간 먼저
|
||||
assert sum(_knee(cut) for cut in tails) >= 2
|
||||
|
||||
|
||||
def test_박스_끝_세월교_측벽_끝_절토_복합경사(fit: dict) -> None:
|
||||
for cuts in (fit["boxEnd"], fit["fordEnd"]):
|
||||
assert cuts and all(_knee(cut) and cut["endGap"] < 0.01 for cut in cuts)
|
||||
|
||||
|
||||
def test_절토_쪽_배관_유출_벽_뒤_절토사면_살아남(fit: dict) -> None:
|
||||
cut = fit["cutOutlet"]
|
||||
assert _knee(cut) and cut["endGap"] < 0.01
|
||||
|
||||
|
||||
def test_소단은_구조물_끝에서_새로_셈(fit: dict) -> None:
|
||||
legs = fit["cutOutletBerm"]["legs"]
|
||||
flat = next(i for i, leg in enumerate(legs) if leg["ratio"] is None)
|
||||
assert sum(leg["slant"] for leg in legs[:flat]) == pytest.approx(3.0, abs=1e-3)
|
||||
assert legs[flat]["run"] == pytest.approx(0.5, abs=1e-3)
|
||||
assert fit["cutOutletBerm"]["endGap"] < 0.01
|
||||
|
||||
Reference in New Issue
Block a user