feat(B06): 관 접속 절·성토선 3도 물매 + 교차점 보간 + 절성토 교차 기준 자동 줌아웃
- 관 유도 접속선을 0도 → **3도**로: 유입측은 계류 쪽 오름(물이 관으로), 유출측 매몰 절토선은 계류 쪽 내림(물이 밖으로). 집수정 I형 되메움선도 동일 - 모든 유도선·성토부선 끝점을 격자점이 아니라 **선형 보간 교차점**에 붙여 선이 원지반을 미세하게 벗어나던 문제 해소(2026-08-22 사용자 지적) - 카드 자동 줌아웃: **구조물 없는 상태**의 설계 절·성토선이 원지반과 만나는 지점(+0.5m)이 표시 반폭 밖이면 그 카드만 반폭을 넓혀 그린다. 보유 샘플 범위를 넘는 경우는 기존 전역 반폭 재생성 경로(applyPanelToAll)가 담당 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
|
||||
import {
|
||||
EXTRA_WALL_DEFAULT_HEIGHT_M,
|
||||
PIPE_CONNECT_GRADE,
|
||||
EXTRA_WALL_MIN_HEIGHT_M,
|
||||
FILL_SLOPE_MAX_LENGTH_M,
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
@@ -56,16 +57,26 @@ export interface OutletExtrasResult {
|
||||
|
||||
/** 수평선(0도)이 계류측으로 나가며 원지반 아래로 다시 떨어지는 첫 지점(절토 교차).
|
||||
* 없으면 null — 그 자리는 매몰 상태로 둘 수 없다(2026-08-22 사용자 ③ 이동 금지). */
|
||||
export function horizontalCrossing(
|
||||
export function slopedCrossing(
|
||||
from: OffsetPoint,
|
||||
outward: number,
|
||||
grade: number,
|
||||
groundAt: (offset: number) => number,
|
||||
limitOffset: number,
|
||||
): number | null {
|
||||
const span = (limitOffset - from.offset) * outward;
|
||||
for (let t = 0.25; t <= span + 1e-9; t += 0.25) {
|
||||
const step = 0.25;
|
||||
let previousGap = groundAt(from.offset) - from.elevation; // 지반 − 선 (양수 = 지반 위)
|
||||
for (let t = step; t <= span + 1e-9; t += step) {
|
||||
const x = from.offset + outward * t;
|
||||
if (groundAt(x) <= from.elevation) return x;
|
||||
const gap = groundAt(x) - (from.elevation + grade * t);
|
||||
if (gap <= 0) {
|
||||
// 격자 사이 정확 교차점을 선형 보간 — 격자점을 그대로 쓰면 선이 지반을
|
||||
// 미세하게 벗어난다(2026-08-22 사용자 지적).
|
||||
const fraction = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
|
||||
return x - outward * step * (1 - fraction);
|
||||
}
|
||||
previousGap = gap;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -85,33 +96,58 @@ export function inletGroundConnector(
|
||||
const ground = groundAt(corner.offset);
|
||||
const span = (limitOffset - corner.offset) * outward;
|
||||
if (corner.elevation > ground + 0.02) {
|
||||
let cross = corner.offset + outward * Math.max(span, 0);
|
||||
// 3도 성토선 — 계류 쪽이 높다(물이 관으로). 지반 교차점(보간)에서 정지.
|
||||
let end: OffsetPoint = {
|
||||
offset: corner.offset + outward * Math.max(span, 0),
|
||||
elevation: corner.elevation + PIPE_CONNECT_GRADE * Math.max(span, 0),
|
||||
};
|
||||
let previousGap = corner.elevation - groundAt(corner.offset); // 선 − 지반
|
||||
for (let t = 0.25; t <= span + 1e-9; t += 0.25) {
|
||||
const x = corner.offset + outward * t;
|
||||
if (groundAt(x) >= corner.elevation) {
|
||||
cross = x;
|
||||
const lineElevation = corner.elevation + PIPE_CONNECT_GRADE * t;
|
||||
const gap = lineElevation - groundAt(x);
|
||||
if (gap <= 0) {
|
||||
const fraction = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
|
||||
const tCross = t - 0.25 * (1 - fraction);
|
||||
end = {
|
||||
offset: corner.offset + outward * tCross,
|
||||
elevation: corner.elevation + PIPE_CONNECT_GRADE * tCross,
|
||||
};
|
||||
break;
|
||||
}
|
||||
previousGap = gap;
|
||||
}
|
||||
return {
|
||||
points: [corner, { offset: cross, elevation: corner.elevation }],
|
||||
lengthM: Math.abs(cross - corner.offset),
|
||||
points: [corner, end],
|
||||
lengthM: Math.hypot(end.offset - corner.offset, end.elevation - corner.elevation),
|
||||
overLimit: false,
|
||||
kind: "fill",
|
||||
};
|
||||
}
|
||||
if (corner.elevation < ground - 0.02) {
|
||||
// 3도 직선 1m(계류 쪽이 높다) 후 표준 절토 경사.
|
||||
const bend: OffsetPoint = {
|
||||
offset: corner.offset + outward * 1.0,
|
||||
elevation: corner.elevation,
|
||||
elevation: corner.elevation + PIPE_CONNECT_GRADE * 1.0,
|
||||
};
|
||||
let last = bend;
|
||||
const cutSpan = (limitOffset - bend.offset) * outward;
|
||||
let previousGap = groundAt(bend.offset) - bend.elevation; // 지반 − 절토선
|
||||
for (let t = 0.25; t <= Math.max(cutSpan, 0) + 1e-9; t += 0.25) {
|
||||
const x = bend.offset + outward * t;
|
||||
const elevation = bend.elevation + t / Math.max(cutSlopeRatio, 0.1);
|
||||
const gap = groundAt(x) - elevation;
|
||||
if (gap <= 0) {
|
||||
const fraction = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
|
||||
const tCross = t - 0.25 * (1 - fraction);
|
||||
last = {
|
||||
offset: bend.offset + outward * tCross,
|
||||
elevation: bend.elevation + tCross / Math.max(cutSlopeRatio, 0.1),
|
||||
};
|
||||
break;
|
||||
}
|
||||
last = { offset: x, elevation };
|
||||
if (elevation >= groundAt(x)) break;
|
||||
previousGap = gap;
|
||||
}
|
||||
return {
|
||||
points: [corner, bend, last],
|
||||
@@ -138,15 +174,28 @@ function trailingFill(
|
||||
let length = 0;
|
||||
let previous = src;
|
||||
const step = 0.25;
|
||||
let previousGap = src.elevation - groundAt(src.offset); // 사면선 − 지반
|
||||
for (let t = step; t <= 30 + 1e-9; t += step) {
|
||||
const offset = src.offset + outward * t;
|
||||
const slopeElevation = src.elevation - t / FILL_SLOPE_RATIO_MIN;
|
||||
const groundElevation = groundAt(offset);
|
||||
const point: OffsetPoint = { offset, elevation: Math.max(slopeElevation, groundElevation) };
|
||||
const gap = slopeElevation - groundAt(offset);
|
||||
if (gap <= 0) {
|
||||
// 끝점은 격자 대신 **보간 교차점**에 붙인다(2026-08-22 사용자 — 미세 이탈 정정).
|
||||
const fraction = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
|
||||
const tCross = t - step * (1 - fraction);
|
||||
const point: OffsetPoint = {
|
||||
offset: src.offset + outward * tCross,
|
||||
elevation: src.elevation - tCross / FILL_SLOPE_RATIO_MIN,
|
||||
};
|
||||
length += Math.hypot(point.offset - previous.offset, point.elevation - previous.elevation);
|
||||
points.push(point);
|
||||
break;
|
||||
}
|
||||
const point: OffsetPoint = { offset, elevation: slopeElevation };
|
||||
length += Math.hypot(point.offset - previous.offset, point.elevation - previous.elevation);
|
||||
points.push(point);
|
||||
previous = point;
|
||||
if (slopeElevation <= groundElevation) break;
|
||||
previousGap = gap;
|
||||
}
|
||||
return {
|
||||
points,
|
||||
@@ -337,11 +386,12 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
const topFrontX = jointX + outward * REVET_THICKNESS_M;
|
||||
const startX = topFrontX + outward * REVET_LEAN_RATIO * (topE - startE);
|
||||
if (groundAt(startX) < startE + 0.05) return true;
|
||||
// 매몰 — 0도 절토선이 원지반과 다시 만나면 허용(2026-08-22 ③), 아니면 이동 금지.
|
||||
// 매몰 — 3도 절토선(계류 쪽 내림)이 원지반과 다시 만나면 허용, 아니면 이동 금지.
|
||||
return (
|
||||
horizontalCrossing(
|
||||
slopedCrossing(
|
||||
{ offset: startX, elevation: startE },
|
||||
outward,
|
||||
-PIPE_CONNECT_GRADE,
|
||||
groundAt,
|
||||
limitOffset,
|
||||
) !== null
|
||||
@@ -413,11 +463,15 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
segments.push(tail);
|
||||
addable = tail.overLimit;
|
||||
} else {
|
||||
const cross = horizontalCrossing(src, outward, groundAt, limitOffset);
|
||||
const cross = slopedCrossing(src, outward, -PIPE_CONNECT_GRADE, groundAt, limitOffset);
|
||||
if (cross !== null) {
|
||||
const end: OffsetPoint = {
|
||||
offset: cross,
|
||||
elevation: src.elevation - PIPE_CONNECT_GRADE * Math.abs(cross - src.offset),
|
||||
};
|
||||
segments.push({
|
||||
points: [src, { offset: cross, elevation: src.elevation }],
|
||||
lengthM: Math.abs(cross - src.offset),
|
||||
points: [src, end],
|
||||
lengthM: Math.hypot(end.offset - src.offset, end.elevation - src.elevation),
|
||||
overLimit: false,
|
||||
kind: "cut",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user