feat(B06): 기슭막이 자동 자리 노견 최소화·수동 이동 독립화
- 자동 자리 = 물매 1:1.2가 딱 성립하는 가장 안쪽(노견 최소) 지점. 관 m단위 올림 잔여만 유출 벽 바깥 이동으로 흡수(minShoulderWallOffset). 사면 끝 밖 자리는 급물매 교정일 때만 인정 — 얕은 성토 폭주 방지. - 손으로 민 벽은 관 정수 맞춤이 절대 옮기지 않음 — 한쪽 이동 시 반대쪽 벽 동시 이동(관 +2m) 소멸. 표기 길이만 올림(m). - 안쪽 당김 한계 = 1:1.2 성립 자리(우선순위: 배관 연장 > 높이 증가 > 바닥 띄움). 1:1.2 자리가 없는 지형 폴백에서만 높이 증가·바닥 띄움 (solveWallVertical, 한계 찰3.0/메2.0). - 기슭막이는 관을 감싼다: 하단은 항상 관 invert 아래 0.5m 확보, 뜬 바닥도 동일(뜬 공간은 별도 지지 구조물 — 추가 예정). - 검증: 4+4.3 관 13m→9m·노폭연장 1.52→0.08, 13+15.7 UI 조작 (◀▶↺·클램프·단독이동) 및 dz 스윕 24건 물매 이탈 0건. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -117,7 +117,9 @@ export function appendCulvertOverlay(
|
||||
`${roleLabel(wall.role)} 기슭막이 ${wall.form ?? ""} H=${wall.height.toFixed(1)}m` +
|
||||
`(상단 = 사면선 접점, 전면 1:${REVET_LEAN_RATIO}` +
|
||||
`, 높이 한계 ${revetHeightLimit(wall.form).toFixed(1)}m — 교본 7-3)` +
|
||||
` · 근입 ${REVET_EMBED_DEPTH_M.toFixed(2)}m(실무 기초콘크리트 H=0.5 — 법정 규정 없음)` +
|
||||
(wall.floatGapM > 0.01
|
||||
? ` · ⚠ 바닥 원지반 이격 ${wall.floatGapM.toFixed(2)}m — 하부 지지 구조물 별도(추가 예정)`
|
||||
: ` · 근입 ${REVET_EMBED_DEPTH_M.toFixed(2)}m(실무 기초콘크리트 H=0.5 — 법정 규정 없음)`) +
|
||||
(wall.lengthM ? ` · 연장 ${wall.lengthM.toFixed(1)}m` : ""),
|
||||
);
|
||||
if (onSelectRevet) revetShape.classList.add("is-selectable");
|
||||
|
||||
@@ -45,17 +45,18 @@ import type {
|
||||
export * from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
export * from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
import type { FillSlopeSegment } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import type { FillSlopeSegment, WallVertical } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import {
|
||||
clampWallOffset,
|
||||
cutLength,
|
||||
enforceMinFillRatio,
|
||||
minShoulderWallOffset,
|
||||
outletSlopeFactory,
|
||||
fillSlopeOf,
|
||||
groundInterpolator,
|
||||
designInterpolator,
|
||||
intersect,
|
||||
slopeToeOffset,
|
||||
solveWallVertical,
|
||||
STRAY_LIMIT_M,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
|
||||
@@ -121,34 +122,51 @@ export function computeCulvertLayout(
|
||||
Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form));
|
||||
/** 요청값이 한계에 걸려 잘린 뒤의 **실제** 이동량. 조정창이 이 값을 되받는다. */
|
||||
const appliedShift = { inlet: 0, outlet: 0 };
|
||||
// 기슭막이 기본 자리 = **성토 사면이 원지반과 만나는 지점**(사면 끝)이고, 그 자리가
|
||||
// 하단선 중점이 된다(2026-08-21 사용자 확정). 사면길이 5m는 자리 한계가 아니라
|
||||
// "5m 이상이면 기슭막이 의무"라는 뜻이라 여기서 자르지 않는다.
|
||||
/** 손으로 민 벽의 수직 제원(높이 증가·바닥 띄움) — buildWall에 물려 준다. */
|
||||
const wallVertical: { inlet: WallVertical | null; outlet: WallVertical | null } = {
|
||||
inlet: null,
|
||||
outlet: null,
|
||||
};
|
||||
// 기슭막이 자동 자리 = **노견(노폭 연장)이 최소가 되는 지점**(2026-08-22 사용자 확정)
|
||||
// — 표준 높이 벽으로 물매 1:1.2가 딱 성립하는 가장 안쪽 자리. 관 m단위 올림 잔여가
|
||||
// 벽을 바깥으로 밀면 그만큼만 노견이 조정된다. 못 찾으면 사면 끝으로 폴백.
|
||||
let inletAutoOffset: number | null = null;
|
||||
if (!basinReason && designAt) {
|
||||
const inletBaseAt = (offset: number): number =>
|
||||
Math.min(groundAt(offset), invertCap(inletInfo.edge));
|
||||
// 사면 끝이 기본 자리. 그 자리 물매가 1:1.2보다 급하면 규정 위반이라 바깥으로 민다
|
||||
// (완만한 쪽은 노폭 연장으로 맞춘다 — fillSlopeOf).
|
||||
inletAutoOffset = enforceMinFillRatio(
|
||||
const inletToe = slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit);
|
||||
const inletFeasible = minShoulderWallOffset(
|
||||
inletInfo.edge,
|
||||
inletInfo.outward,
|
||||
wallHeightFor(culvert.inlet),
|
||||
inletBaseAt,
|
||||
slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit),
|
||||
inletToe,
|
||||
inletInfo.limit,
|
||||
);
|
||||
inletAutoOffset = inletFeasible ?? inletToe;
|
||||
// 안쪽 당김 한계 = 1:1.2 성립 자리(= 자동 자리). 그런 자리가 없는 지형만 노견까지
|
||||
// 허용하고, 그때 높이 증가·바닥 띄움이 발동한다(2026-08-22 사용자 확정).
|
||||
const shifted = clampWallOffset(
|
||||
inletAutoOffset,
|
||||
inletAutoOffset + inletInfo.outward * (revetShift?.inlet ?? 0),
|
||||
wallHeightFor(culvert.inlet),
|
||||
inletInfo.edge,
|
||||
inletBaseAt,
|
||||
inletFeasible ?? inletInfo.edge.offset_m,
|
||||
inletInfo.outward,
|
||||
);
|
||||
appliedShift.inlet = Math.round((shifted - inletAutoOffset) * inletInfo.outward * 10) / 10;
|
||||
// 안쪽으로 당겨 노견이 부족하면 ① 높이 증가 ② 바닥 띄움(2026-08-22 사용자 확정).
|
||||
const vertical = solveWallVertical(
|
||||
shifted,
|
||||
inletInfo.edge,
|
||||
inletInfo.outward,
|
||||
inletBaseAt(shifted),
|
||||
wallHeightFor(culvert.inlet),
|
||||
revetHeightLimit(culvert.inlet.revet_form),
|
||||
);
|
||||
// 뜬 바닥(= 관 invert)이 노면−관경−토피 상한을 넘으면 토피가 부족하다 — 상한으로 자른다.
|
||||
vertical.baseElevation = Math.min(vertical.baseElevation, invertCap(inletInfo.edge));
|
||||
vertical.floatGapM = Math.max(0, vertical.baseElevation - inletBaseAt(shifted));
|
||||
wallVertical.inlet = vertical;
|
||||
inlet.offset = shifted;
|
||||
inlet.elevation = inletBaseAt(shifted);
|
||||
inlet.elevation = vertical.baseElevation;
|
||||
}
|
||||
|
||||
// 유출 목표점 = 유출측 성토사면이 지반과 만나는 사면 끝(경사길이 5m 한계 — 별표2).
|
||||
@@ -215,6 +233,7 @@ export function computeCulvertLayout(
|
||||
outward: number,
|
||||
forceBasinReason: BasinLayout["reason"] | null,
|
||||
thickness: number = REVET_THICKNESS_M,
|
||||
vertical: WallVertical | null = null,
|
||||
): WallLayout | null => {
|
||||
const reason: BasinLayout["reason"] | null =
|
||||
forceBasinReason ?? (spec.structure === "집수정" ? "cut" : null);
|
||||
@@ -347,12 +366,14 @@ export function computeCulvertLayout(
|
||||
// 형상(사용자 스케치 확정 — 좌측 벽 기준, 우측은 반전): 배면(도로측) 수직, 전면
|
||||
// (계류측)은 1:0.3 기운 평행사변형 띠. 띠 안쪽 평행선과 수직 배면 사이가 사다리꼴로,
|
||||
// 바닥이 넓고 상단이 좁다(상단 폭 = 띠 두께 0.45).
|
||||
// 높이: **관경 + 여유고**를 0.1m 눈금으로 올린 값(2026-08-21 사용자 확정). 관이
|
||||
// 물매를 두어 좌·우 invert가 달라도 **관 상단 위 여유고는 같아야** 한다. 사면선은
|
||||
// 높이를 정하지 않고 벽이 설 **자리**만 정한다(`solveFillWallOffset`).
|
||||
// 높이: 기본은 **관경 + 여유고**를 0.1m 눈금으로 올린 값(2026-08-21 사용자 확정).
|
||||
// 손으로 안쪽에 당겨 노견이 부족하면 수직 제원(vertical)이 높이를 키우거나 바닥을
|
||||
// 띄워 들어온다(2026-08-22 사용자 확정 — 최소 노견 유지).
|
||||
// 형태별 법정·교본 높이 한계(찰 3.0 / 메 2.0 — 돌쌓기.md §1)를 넘지 못한다.
|
||||
const height = Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form));
|
||||
const height =
|
||||
vertical?.height ?? Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form));
|
||||
if (!(height > 0.05)) return null;
|
||||
const floatGapM = vertical?.floatGapM ?? 0;
|
||||
// **자리 기준 = 하단선 중점**(2026-08-21 사용자 확정). 배면 기준으로 잡으면 벽이
|
||||
// 기울 때 체감 위치가 어긋난다. anchor.offset이 하단선 중점, anchor.elevation이
|
||||
// 그 자리 원지반(= 관 invert)이다.
|
||||
@@ -366,7 +387,14 @@ export function computeCulvertLayout(
|
||||
const topFront = topJoint + outward * thickness;
|
||||
// 근입: 하단선은 **원지반을 수직으로 0.5m 내린 선**이다(2026-08-21 사용자 확정).
|
||||
// 종전에는 수평 바닥이라 사면에서 앞 모서리가 떴다.
|
||||
const bottomAt = (offset: number): number => groundAt(offset) - REVET_EMBED_DEPTH_M;
|
||||
// 기슭막이는 **배관을 묻고 보강하는 구조물 — 관을 감싸야 한다**(2026-08-22 사용자
|
||||
// 확정): 하단은 어떤 경우에도 관 하단(invert = anchor.elevation) 아래 0.5m를
|
||||
// 확보한다. 관이 지반보다 낮게 묻힌 자리는 invert−0.5가 지반−0.5보다 깊고,
|
||||
// 바닥이 원지반에서 뜬 벽(floatGapM>0)도 관 기준 0.5는 그대로 내려간다
|
||||
// (뜬 공간은 별도 지지 구조물 — 추가 예정).
|
||||
const bottomAt = (offset: number): number =>
|
||||
(floatGapM > 1e-6 ? anchor.elevation : Math.min(groundAt(offset), anchor.elevation)) -
|
||||
REVET_EMBED_DEPTH_M;
|
||||
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomAt(backOffset) };
|
||||
const bottomFront: OffsetPoint = { offset: frontBase, elevation: bottomAt(frontBase) };
|
||||
const wall: WallLayout = {
|
||||
@@ -377,6 +405,7 @@ export function computeCulvertLayout(
|
||||
outerOffset: frontBase,
|
||||
base: anchor.elevation,
|
||||
height,
|
||||
floatGapM,
|
||||
outward,
|
||||
topBack,
|
||||
topJoint: { offset: topJoint, elevation: topElevation },
|
||||
@@ -403,7 +432,14 @@ export function computeCulvertLayout(
|
||||
};
|
||||
return wall;
|
||||
};
|
||||
buildWall(culvert.inlet, inlet, inletInfo.outward, basinReason);
|
||||
buildWall(
|
||||
culvert.inlet,
|
||||
inlet,
|
||||
inletInfo.outward,
|
||||
basinReason,
|
||||
REVET_THICKNESS_M,
|
||||
wallVertical.inlet,
|
||||
);
|
||||
// 유출 벽 자리 — 유입과 같은 규칙(사면선이 벽 이음선 상단점을 지나는 자리, 물매는
|
||||
// 1:1.2~2.0 범위에서 역산).
|
||||
//
|
||||
@@ -412,31 +448,49 @@ export function computeCulvertLayout(
|
||||
// (2026-08-21 사용자 지적). 관 끝은 원지반 위라는 원칙대로 지반을 따르면 벽이
|
||||
// 지반에 앉고 관 물매가 그 자리에 맞춰진다. 역경사는 유입 invert로 클램프.
|
||||
const invertAt = (offset: number): number => Math.min(groundAt(offset), inlet.elevation);
|
||||
// 유출 벽 기본 자리도 **사면 끝**(= outletAnchorOffset)이고 그 자리가 하단선 중점이다.
|
||||
// 유출 벽 자동 자리도 **노견 최소 지점**(2026-08-22 사용자 확정) — 못 찾으면 사면 끝.
|
||||
let outletWallAnchor = outletAnchor;
|
||||
const outletAutoOffset = designAt
|
||||
? enforceMinFillRatio(
|
||||
const outletFeasible = designAt
|
||||
? minShoulderWallOffset(
|
||||
outletInfo.edge,
|
||||
outletInfo.outward,
|
||||
wallHeightFor(culvert.outlet),
|
||||
(offset) => Math.min(groundAt(offset), inlet.elevation),
|
||||
invertAt,
|
||||
outletAnchorOffset,
|
||||
outletInfo.limit,
|
||||
)
|
||||
: outletAnchorOffset;
|
||||
: null;
|
||||
const outletAutoOffset = outletFeasible ?? outletAnchorOffset;
|
||||
if (designAt) {
|
||||
// 안쪽 당김 한계 = 1:1.2 성립 자리(유입과 동일 규칙 — 2026-08-22 사용자 확정).
|
||||
const shifted = clampWallOffset(
|
||||
outletAutoOffset,
|
||||
outletAutoOffset + outletInfo.outward * (revetShift?.outlet ?? 0),
|
||||
wallHeightFor(culvert.outlet),
|
||||
outletInfo.edge,
|
||||
invertAt,
|
||||
outletFeasible ?? outletInfo.edge.offset_m,
|
||||
outletInfo.outward,
|
||||
);
|
||||
appliedShift.outlet = Math.round((shifted - outletAutoOffset) * outletInfo.outward * 10) / 10;
|
||||
outletWallAnchor = { offset: shifted, elevation: invertAt(shifted) };
|
||||
const vertical = solveWallVertical(
|
||||
shifted,
|
||||
outletInfo.edge,
|
||||
outletInfo.outward,
|
||||
invertAt(shifted),
|
||||
wallHeightFor(culvert.outlet),
|
||||
revetHeightLimit(culvert.outlet.revet_form),
|
||||
);
|
||||
// 뜬 바닥(= 유출 invert)이 유입 invert보다 높으면 역경사다 — 유입 높이로 자른다.
|
||||
vertical.baseElevation = Math.min(vertical.baseElevation, inlet.elevation);
|
||||
vertical.floatGapM = Math.max(0, vertical.baseElevation - invertAt(shifted));
|
||||
wallVertical.outlet = vertical;
|
||||
outletWallAnchor = { offset: shifted, elevation: vertical.baseElevation };
|
||||
}
|
||||
let outletWall = buildWall(culvert.outlet, outletWallAnchor, outletInfo.outward, null);
|
||||
let outletWall = buildWall(
|
||||
culvert.outlet,
|
||||
outletWallAnchor,
|
||||
outletInfo.outward,
|
||||
null,
|
||||
REVET_THICKNESS_M,
|
||||
wallVertical.outlet,
|
||||
);
|
||||
// 유출 벽이 사면 끝보다 안쪽으로 당겨졌으면 **관 끝도 그 자리에 맞춘다** — 관만 사면
|
||||
// 끝까지 남으면 벽과 떨어져 마감면 교차가 실패한다(2026-08-20 사용자 ② 원인).
|
||||
if (outletWall && Math.abs(outletWallAnchor.offset - outletAnchor.offset) > 0.05) {
|
||||
@@ -496,7 +550,11 @@ export function computeCulvertLayout(
|
||||
const outletSlopeAt = outletSlopeFactory(outletInfo, invertAt, wallHeightFor(culvert.outlet));
|
||||
/** 유출 벽 두께 — 항상 0.45 고정(2026-08-21 사용자 확정). 폭으로 길이를 맞추지 않는다. */
|
||||
const outletThickness = REVET_THICKNESS_M;
|
||||
if (outletWall) {
|
||||
// 손으로 민 벽은 정수 맞춤이 **절대 옮기지 않는다**(2026-08-22 사용자 확정 — 종전에는
|
||||
// 한쪽 벽을 밀면 이 루프가 반대쪽 벽을 따라 옮겨 좌우가 동시에 움직였다). 관 그림은
|
||||
// 두 벽 전면 사이 실길이로 두고 표기 길이만 올림(m)한다.
|
||||
const outletPinned = Math.abs(revetShift?.outlet ?? 0) > 1e-9;
|
||||
if (outletWall && !outletPinned) {
|
||||
const target = Math.ceil(cutLength(pipeCorners) - 1e-6);
|
||||
const rebuild = (): void => {
|
||||
walls.pop();
|
||||
@@ -528,15 +586,12 @@ export function computeCulvertLayout(
|
||||
) {
|
||||
break;
|
||||
}
|
||||
// 조정창으로 옮긴 벽은 **폭을 건드리지 않는다** — 폭으로 흡수하면 미는 동안 벽
|
||||
// 단면이 변한다(2026-08-21 사용자 지적: 유출 벽만 0.45→0.90). 조정이 관 길이 1m
|
||||
// 단위라 자리만 맞추면 정수가 성립한다. 물매·5m 한계는 자동 배치용 제약이라
|
||||
// 사용자가 지정한 자리에는 걸지 않는다(경고로 알린다).
|
||||
// 자동 자리만 옮긴다(수동 벽은 이 루프에 아예 안 들어온다). 폭은 0.45 고정 —
|
||||
// 폭으로 흡수하면 미는 동안 벽 단면이 변한다(2026-08-21 사용자 지적).
|
||||
if (
|
||||
Math.abs(revetShift?.outlet ?? 0) > 1e-9 ||
|
||||
(movedSlope.ratio >= FILL_SLOPE_RATIO_MIN &&
|
||||
movedSlope.ratio <= FILL_SLOPE_RATIO_MAX &&
|
||||
movedSlope.lengthM <= FILL_SLOPE_MAX_LENGTH_M + 1e-6)
|
||||
movedSlope.ratio >= FILL_SLOPE_RATIO_MIN &&
|
||||
movedSlope.ratio <= FILL_SLOPE_RATIO_MAX &&
|
||||
movedSlope.lengthM <= FILL_SLOPE_MAX_LENGTH_M + 1e-6
|
||||
) {
|
||||
outletWallAnchor = { offset: moved, elevation: invertAt(moved) };
|
||||
rebuild();
|
||||
@@ -549,7 +604,10 @@ export function computeCulvertLayout(
|
||||
// 남는 몫은 관이 벽을 조금 벗어나는 것으로 두는 게 기존 확정 규칙이다.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (outletWall) {
|
||||
// 표기 길이 = **실제 그려진 관**의 올림값(목표를 적으면 도면보다 길게 적힌다).
|
||||
// 수동 벽으로 실길이가 정수가 아니어도 표기만 올림한다(2026-08-22 사용자 확정).
|
||||
lengthM = Math.max(1, Math.ceil(cutLength(pipeCorners) - 1e-6));
|
||||
slopePct =
|
||||
Math.abs(outlet.offset - pipeStart.offset) > 1e-9
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FILL_MIN_RISE_M,
|
||||
FILL_SLOPE_MAX_LENGTH_M,
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
REVET_HEIGHT_STEP_M,
|
||||
REVET_LEAN_RATIO,
|
||||
REVET_THICKNESS_M,
|
||||
REVET_TRAP_TOP_M,
|
||||
@@ -211,64 +212,99 @@ export function outletSlopeFactory(
|
||||
}
|
||||
|
||||
/**
|
||||
* 손으로 민 벽 자리를 **성토가 성립하는 범위**로 되돌린다.
|
||||
* · 벽은 제 노견 바깥에 선다(도로 밑을 파고들면 안 된다)
|
||||
* · 성토고 하한(FILL_MIN_RISE_M) — 그 아래면 사면선이 역전된다
|
||||
* · 안쪽으로 당길 때는 **물매 1:1.2**를 넘지 않는 자리까지만(2026-08-21 사용자 지시).
|
||||
* 바깥으로 밀 때는 물매가 1:1.2로 고정된 채 노폭이 늘어나므로 막지 않는다.
|
||||
* 손으로 민 벽 자리 한계 — 안쪽 당김은 **1:1.2가 성립하는 가장 안쪽 자리**(innerLimit
|
||||
* = 자동 자리)에서 멈춘다(2026-08-22 사용자 확정 — 우선순위: 배관 연장 > 높이 증가 >
|
||||
* 바닥 띄움. 당겨서 물매가 깨질 바에는 관을 길게 유지한다). 1:1.2 자리가 아예 없는
|
||||
* 지형 폴백에서는 innerLimit이 노견까지 내려오고, 그때만 높이 증가·바닥 띄움
|
||||
* (`solveWallVertical`)이 발동한다. 바깥 밀기는 막지 않는다(노폭 연장으로 흡수).
|
||||
*/
|
||||
export function clampWallOffset(
|
||||
autoOffset: number,
|
||||
wantedOffset: number,
|
||||
height: number,
|
||||
edge: { offset_m: number; elevation_m: number },
|
||||
baseAt: (offset: number) => number,
|
||||
innerLimitOffset: number,
|
||||
outward: number,
|
||||
): number {
|
||||
const steps = 40;
|
||||
const inward = (wantedOffset - autoOffset) * outward < 0;
|
||||
let allowed = autoOffset;
|
||||
for (let i = 1; i <= steps; i += 1) {
|
||||
const candidate = autoOffset + ((wantedOffset - autoOffset) * i) / steps;
|
||||
if ((candidate - edge.offset_m) * outward < 0) break;
|
||||
const rise = edge.elevation_m - (baseAt(candidate) + height);
|
||||
if (rise < FILL_MIN_RISE_M) break;
|
||||
if (inward) {
|
||||
const joint = candidate - outward * (fillWallBaseWidth(height) / 2 - REVET_TRAP_TOP_M);
|
||||
if (Math.abs(joint - edge.offset_m) / rise < FILL_SLOPE_RATIO_MIN) break;
|
||||
}
|
||||
allowed = candidate;
|
||||
}
|
||||
return allowed;
|
||||
return (wantedOffset - innerLimitOffset) * outward < 0 ? innerLimitOffset : wantedOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사면 끝 자리의 물매가 **1:1.2보다 급하면** 규정 위반이라 1:1.2가 될 때까지 바깥으로
|
||||
* 민다. 반대로 완만한 쪽(1:1.2보다 느슨)은 `fillSlopeOf`가 **노폭을 늘려** 맞추므로
|
||||
* 여기서 건드리지 않는다(2026-08-21 사용자 확정 — 물매는 1:1.2 고정).
|
||||
* 지반이 1:1.2보다 급해 끝내 못 맞추면 시작 자리를 돌려주고 경고로 알린다.
|
||||
* 기슭막이 **자동 자리 = 노견(노폭 연장)이 최소가 되는 지점**(2026-08-22 사용자 확정).
|
||||
* 노견 끝에서 바깥으로 훑어, 표준 높이 벽으로 ① 성토고 하한(FILL_MIN_RISE_M)과
|
||||
* ② 물매 1:1.2 이상(급하지 않음)을 처음 만족하는 자리를 돌려준다 — 그 자리가
|
||||
* 물매 1:1.2가 딱 성립하는 가장 안쪽 자리라 노폭 연장이 0에 수렴한다.
|
||||
* (관 m단위 올림 잔여가 벽을 바깥으로 밀면 그만큼만 노견이 조정된다.)
|
||||
*
|
||||
* 사면 끝(toeOffset) **밖**의 자리는 사면 끝에 성토고가 서 있는데 물매만 급한 경우
|
||||
* (규정 위반 교정)만 인정한다 — 성토가 얕아 자리 자체가 안 서는 지형에서 절벽 너머로
|
||||
* 폭주하는 것을 막는다(계획고 인하 스윕에서 노폭 연장 11m 관측). 못 찾으면 null —
|
||||
* 호출부가 사면 끝으로 폴백한다.
|
||||
*/
|
||||
export function enforceMinFillRatio(
|
||||
export function minShoulderWallOffset(
|
||||
edge: { offset_m: number; elevation_m: number },
|
||||
outward: number,
|
||||
height: number,
|
||||
baseAt: (offset: number) => number,
|
||||
startOffset: number,
|
||||
toeOffset: number,
|
||||
limitOffset: number,
|
||||
): number {
|
||||
const ratioAt = (offset: number): number => {
|
||||
const rise = edge.elevation_m - (baseAt(offset) + height);
|
||||
if (!(rise > 1e-6)) return Number.POSITIVE_INFINITY;
|
||||
const joint = offset - outward * (fillWallBaseWidth(height) / 2 - REVET_TRAP_TOP_M);
|
||||
return Math.abs(joint - edge.offset_m) / rise;
|
||||
};
|
||||
if (ratioAt(startOffset) >= FILL_SLOPE_RATIO_MIN) return startOffset;
|
||||
const span = (limitOffset - startOffset) * outward;
|
||||
if (!(span > 0)) return startOffset;
|
||||
const steps = 200;
|
||||
): number | null {
|
||||
const span = (limitOffset - edge.offset_m) * outward;
|
||||
if (!(span > 0)) return null;
|
||||
const steps = 240;
|
||||
for (let i = 1; i <= steps; i += 1) {
|
||||
const candidate = startOffset + outward * ((span * i) / steps);
|
||||
if (ratioAt(candidate) >= FILL_SLOPE_RATIO_MIN) return candidate;
|
||||
const offset = edge.offset_m + outward * ((span * i) / steps);
|
||||
const rise = edge.elevation_m - (baseAt(offset) + height);
|
||||
if (rise < FILL_MIN_RISE_M) continue;
|
||||
const joint = offset - outward * (fillWallBaseWidth(height) / 2 - REVET_TRAP_TOP_M);
|
||||
if (((joint - edge.offset_m) * outward) / rise < FILL_SLOPE_RATIO_MIN) continue;
|
||||
if ((offset - toeOffset) * outward > 1e-6) {
|
||||
const toeRise = edge.elevation_m - (baseAt(toeOffset) + height);
|
||||
if (toeRise < FILL_MIN_RISE_M) return null; // 성토 얕음 — 사면 끝 폴백
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
return startOffset;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 벽의 수직 제원 — 높이·바닥 표고·바닥이 원지반에서 뜬 간격. */
|
||||
export interface WallVertical {
|
||||
height: number;
|
||||
baseElevation: number;
|
||||
/** 바닥~원지반 간격(m). 0이면 지반 위에 선다. 뜬 공간은 별도 구조물 예정. */
|
||||
floatGapM: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 벽 자리에 맞는 **수직 제원**을 푼다(2026-08-22 사용자 확정 — 최소 노견 유지 규칙).
|
||||
* 벽 상단(이음선 상단점)은 기본 노견에서 내려오는 **1:1.2 사면선 위**에 놓여야 한다.
|
||||
* ① 자리가 안쪽이라 표준 높이(관경+여유고)로 사면선에 못 닿으면 → 높이를 키운다.
|
||||
* ② 형태별 높이 한계(찰 3.0/메 2.0)에 걸리면 → 바닥을 원지반에서 띄운다
|
||||
* (뜬 공간은 별도 지지 구조물 — 추가 예정).
|
||||
* ③ 자리가 바깥이라 사면선이 표준 높이 아래로 지나면 → 표준 높이 유지, 모자란
|
||||
* 물매는 `fillSlopeOf`가 노폭 연장으로 맞춘다(기존 규칙).
|
||||
* 높이는 0.1m 눈금 올림(2026-08-21 확정)이라 상단이 사면선보다 반 눈금쯤 높을 수 있다.
|
||||
*/
|
||||
export function solveWallVertical(
|
||||
anchorOffset: number,
|
||||
edge: { offset_m: number; elevation_m: number },
|
||||
outward: number,
|
||||
groundBase: number,
|
||||
minHeight: number,
|
||||
limitHeight: number,
|
||||
): WallVertical {
|
||||
const gridUp = (value: number): number =>
|
||||
Math.ceil(value / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M;
|
||||
let height = minHeight;
|
||||
let desiredTop = groundBase + minHeight;
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const joint = anchorOffset - outward * (fillWallBaseWidth(height) / 2 - REVET_TRAP_TOP_M);
|
||||
const run = Math.max(0, (joint - edge.offset_m) * outward);
|
||||
desiredTop = edge.elevation_m - run / FILL_SLOPE_RATIO_MIN;
|
||||
const next = Math.min(Math.max(gridUp(desiredTop - groundBase), minHeight), limitHeight);
|
||||
if (Math.abs(next - height) < 1e-9) {
|
||||
height = next;
|
||||
break;
|
||||
}
|
||||
height = next;
|
||||
}
|
||||
const floatGap = Math.max(0, desiredTop - limitHeight - groundBase);
|
||||
return { height, baseElevation: groundBase + floatGap, floatGapM: floatGap };
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ export interface WallLayout {
|
||||
outerOffset: number;
|
||||
base: number;
|
||||
height: number;
|
||||
/**
|
||||
* 바닥이 원지반에서 뜬 간격(m, 2026-08-22 사용자 확정). 손으로 안쪽에 당겨 높이
|
||||
* 한계로도 사면선에 못 닿을 때 바닥을 띄워 최소 노견을 확보한다 — 뜬 공간은
|
||||
* 별도 지지 구조물로 메운다(추가 예정). 0이면 지반 위(근입 0.5m).
|
||||
*/
|
||||
floatGapM: number;
|
||||
outward: number;
|
||||
/** 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점 — 데이터 좌표. */
|
||||
points: OffsetPoint[];
|
||||
|
||||
Reference in New Issue
Block a user