관 하단 자리는 벽 계류측 변과 원지반의 교점인데, 원지반이 벽 상단보다 높은 자리에서는 교점이 벽 상단으로 밀려 관이 벽 위에 얹혔다(측벽과 배관이 만나지 않음). ㄴ·ㄷ형은 관이 바닥판 윗면에서 시작해 벽 안에 물리는 것과 대비된다. 교점을 벽 상단에서 관경+여유(0.5m)만큼 아래로 제한한다. 원지반이 그보다 낮으면 종전대로 교점을 그대로 쓴다. 벽을 키우는 방향은 벽이 1:0.3으로 기울어 base가 계류측으로 밀리며 지반 교점이 함께 올라가 벽이 7.5m까지 자라 폐기했다. 검증 — 공용 브라우저 B06 I형 8개 단면 실측: 관 유입측 끝단과 벽의 세로 겹침 18.1~56.4px(수정 전 0 — 벽 위로 이탈). ㄴ형 대조(13+4.1을 조정창에서 ㄴ형으로 바꿔 확인) 겹침 유지, 확인 후 세션값 원복. pytest 370 passed·17 skipped, typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
449 lines
20 KiB
TypeScript
449 lines
20 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Cross_Culvert_Basin.ts
|
||
* 유입 **집수정** 단면 구성 — 기하 본체(`_Cross_Culvert_Geom.ts`)에서 분리(700줄 제한).
|
||
*
|
||
* 집수정은 기슭막이와 달리 **물을 모으려고 만든 구조물**이다(2026-08-22 사용자 확정):
|
||
* · I형(공유벽뿐): 배관은 원지반 자리 유지. 기본 높이(관경+여유)로 노견 끝 접점이
|
||
* 안 되면 벽을 노견 표고까지 키운다.
|
||
* · ㄴ(L)형(= I형 + 바닥판, 기본값): 내부 높이 1.2m(임시값) 등 사이즈를 유지한 채
|
||
* 통째로 노견 끝점에 일치한다 — 바닥(=관 유입 invert)이 따라 올라 관 물매가 바뀐다.
|
||
* · ㄷ(U)형: ㄴ형 + 반대측(도로측) 막음벽(I형 수직반전).
|
||
* 부재(벽·바닥판) 두께 0.2m(2026-08-22 사용자 지정). 벽 자리 = 상단 도로측 꼭짓점이
|
||
* **노견 끝 지점**에 닿는 자리(우측 집수정이면 상단 좌측점).
|
||
* ========================================================================== */
|
||
|
||
import {
|
||
PIPE_CONNECT_GRADE,
|
||
BASIN_INNER_HEIGHT_M,
|
||
BASIN_MEMBER_THICKNESS_M,
|
||
REVET_FREEBOARD_M,
|
||
REVET_LEAN_RATIO,
|
||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||
import { minShoulderWallOffset, slopeToeOffset } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||
import type {
|
||
BasinLayout,
|
||
BasinAdjust,
|
||
BasinShape,
|
||
EndFace,
|
||
OffsetPoint,
|
||
InletStructureChoice,
|
||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||
|
||
export function resolveBasinChoice(
|
||
choice: InletStructureChoice,
|
||
ruleReason: BasinLayout["reason"] | null,
|
||
): { reason: BasinLayout["reason"] | null; shape: BasinShape } {
|
||
const selected = choice === "I" || choice === "L" || choice === "U";
|
||
return {
|
||
reason: choice === "revet" ? null : selected ? (ruleReason ?? "manual") : ruleReason,
|
||
// 자동(기본) 집수정은 **I형**(2026-08-23 사용자) — 종전 L형 기본은 ㄴ·ㄷ형이
|
||
// 선택지에서 숨는 지형(basinLUAllowed=false)에서 선택지 밖 구조물이 그려졌다.
|
||
shape: selected ? choice : "I",
|
||
};
|
||
}
|
||
|
||
export function basinApproachPoints(
|
||
edge: { offset_m: number; elevation_m: number },
|
||
outward: number,
|
||
adjust: BasinAdjust,
|
||
trim: OffsetPoint,
|
||
): OffsetPoint[] | null {
|
||
// 접근선은 **설계선이 벽 상단까지 따라올 경로**다. 이 선을 주지 않으면 설계선이
|
||
// 자기 성토 물매(1:1.2)로 내려갔다가 트림 표고(벽 상단)로 되튀어 올라 급경사
|
||
// 선이 남는다(2026-08-22 사용자 지적). 좌우만 옮겨도 반드시 준다 — 집수정이
|
||
// 나간 만큼 노견이 수평으로 연장되는 것이 맞는 표현이다.
|
||
if (adjust.lateralM <= 1e-6 && adjust.slopeM <= 1e-6) return null;
|
||
const points: OffsetPoint[] = [{ offset: edge.offset_m, elevation: edge.elevation_m }];
|
||
if (adjust.lateralM > 1e-6) {
|
||
points.push({
|
||
offset: edge.offset_m + outward * adjust.lateralM,
|
||
elevation: edge.elevation_m,
|
||
});
|
||
}
|
||
// 중복 꼭짓점 제거 — 수평 선반 끝과 벽 상단이 겹치면 길이 0 구간이 남는다.
|
||
const last = points[points.length - 1];
|
||
if (Math.hypot(trim.offset - last.offset, trim.elevation - last.elevation) > 1e-6) {
|
||
points.push(trim);
|
||
}
|
||
return points.length >= 2 ? points : null;
|
||
}
|
||
|
||
/** 집수정 구성 입력 — 기하 본체가 자기 상태를 명시적으로 물려 준다. */
|
||
export interface BasinBuildInput {
|
||
/** 관 유입 기준점(노견 끝 offset, invert 표고 — ㄴ·ㄷ형은 상승분 반영 후). */
|
||
anchor: OffsetPoint;
|
||
/** 계류측 방향 부호(+ = 좌). */
|
||
outward: number;
|
||
shape: BasinShape;
|
||
reason: BasinLayout["reason"];
|
||
/** 관 외경 지름(m). */
|
||
diameterM: number;
|
||
/** 이 측의 노견 끝 점 — 벽 상단 접점·I형 높이 성장 기준. */
|
||
edge: { offset_m: number; elevation_m: number };
|
||
groundAt: (offset: number) => number;
|
||
/** 표준단면 절토 경사(1:n) — 절토 계획선용. */
|
||
cutSlopeRatio: number;
|
||
adjust: BasinAdjust;
|
||
/** 벽 두께(m). 기본 = 집수정 부재 두께. 세월교 측벽이 다른 두께를 쓸 때 넘긴다. */
|
||
memberThicknessM?: number;
|
||
/** 바닥판 두께(m). 기본 = 벽 두께. 세월교는 교본 물받이 최소 0.3m를 쓴다. */
|
||
floorThicknessM?: number;
|
||
/**
|
||
* 벽 기움(1:n의 n). 기본 = 집수정 규칙 0.3. **세월교는 0**(수직) — 기움은 집수정
|
||
* 부재를 재사용하며 딸려 온 값이고, 교본 그림 3-18 세월교 단면은 수직 벽이다
|
||
* (2026-08-30 사용자 확정: 집수정은 그대로 두고 세월교만 세운다).
|
||
*/
|
||
leanRatio?: number;
|
||
}
|
||
|
||
/** 집수정 구성 결과 — 본체가 트림·관 끝단면·관 시작점에 나눠 꽂는다. */
|
||
export interface BasinBuildResult {
|
||
basin: BasinLayout;
|
||
/** 관 끝단 마감면 = I형 벽의 안쪽(물받는 쪽) 변 복사(2026-08-22 사용자 확정). */
|
||
endFace: EndFace;
|
||
/** 관 시작점(하단 기준) — ㄴ·ㄷ형은 측벽 안쪽 변×바닥 윗면 교차점, I형은 내공 1.0m. */
|
||
pipeEnd: OffsetPoint;
|
||
/** 성토 사면선 트림 경계(벽 상단 도로측 꼭짓점)와 그 표고. */
|
||
trimOffset: number;
|
||
trimElevation: number;
|
||
/**
|
||
* 계류측 성토부선 시작점(2026-08-22 사용자 확정 — 유출측과 같은 체계).
|
||
* ㄴ·ㄷ형에서 구조물 계류측 끝이 **원지반 위**로 나올 때만 준다. 그 자리에서
|
||
* 1:1.2 성토부선을 긋고 5m를 넘으면 다단 기슭막이를 둘 수 있다. 원지반 안으로
|
||
* 박히면 종전대로 절토선(cutLine)이고, I형은 관 하단 되메움선이 맡는다.
|
||
*/
|
||
fillStart: OffsetPoint | null;
|
||
/** 구조물 바닥 표고 — 다단 1단이 이 아래를 뚫지 못하게 하는 기준. */
|
||
fillBottomElevation: number;
|
||
}
|
||
|
||
/** 실무 내공 폭(m) — 울진 돌집수정. I형 관 시작점(노견+1.0m) 계산에도 쓴다. */
|
||
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;
|
||
/** 이 부재의 기움 — 집수정 0.3, 세월교 0(수직). */
|
||
const lean = input.leanRatio ?? REVET_LEAN_RATIO;
|
||
const movedAnchor = {
|
||
offset: baseAnchor.offset + outward * (adjust.lateralM + adjust.slopeM),
|
||
elevation: baseAnchor.elevation - adjust.slopeM / 1.2,
|
||
};
|
||
const anchor = movedAnchor;
|
||
const memberT = input.memberThicknessM ?? BASIN_MEMBER_THICKNESS_M;
|
||
const floorThickness = input.floorThicknessM ?? memberT;
|
||
// 벽 높이: I형은 노견 접점까지 성장, ㄴ·ㄷ형은 내부 높이 1.2m 고정(임시값).
|
||
const wallHeight =
|
||
shape === "I"
|
||
? Math.max(diameterM + REVET_FREEBOARD_M, edge.elevation_m - anchor.elevation)
|
||
: adjust.innerHeightM;
|
||
// 기움은 **수직 기준 반전** — 상단이 도로측(내공 쪽)으로 1:0.3 물러난다(2026-08-20).
|
||
// bottomElevation을 내리면 면 기울기를 그대로 연장한다(I형 근입용).
|
||
const defaultBottom = anchor.elevation - floorThickness;
|
||
const wallOf = (
|
||
baseOffset: number,
|
||
dir: number,
|
||
bottomElevation = defaultBottom,
|
||
): OffsetPoint[] => {
|
||
const topShift = -dir * lean * wallHeight;
|
||
const spanZ = wallHeight + floorThickness;
|
||
const xAt = (elevation: number): number =>
|
||
baseOffset + topShift * ((elevation - defaultBottom) / spanZ);
|
||
return [
|
||
{ offset: xAt(bottomElevation), elevation: bottomElevation },
|
||
{ offset: baseOffset + topShift, elevation: anchor.elevation + wallHeight },
|
||
{
|
||
offset: baseOffset + topShift + dir * memberT,
|
||
elevation: anchor.elevation + wallHeight,
|
||
},
|
||
{
|
||
offset: xAt(bottomElevation) + dir * memberT,
|
||
elevation: bottomElevation,
|
||
},
|
||
];
|
||
};
|
||
// I형 벽 자리 = **상단 도로측 꼭짓점이 노견 끝 지점에 닿는다**(2026-08-22 사용자
|
||
// 확정). 벽 상단이 도로측으로 1:0.3 물러나므로 하단(base)은 그만큼 계류측 바깥.
|
||
const wallBaseAt = (height: number): number => anchor.offset + outward * (lean * height);
|
||
// I형 벽 하단은 **원지반까지 묻는다**(2026-08-22 사용자 — 지반 아래 부재두께만큼
|
||
// 근입 후 배관을 둔다). ㄴ·ㄷ형은 바닥판이 원지반을 대체하므로 현행 유지.
|
||
const iBottomAt = (base: number): number =>
|
||
Math.min(anchor.elevation, groundAt(base), groundAt(base + outward * memberT)) - floorThickness;
|
||
/** 벽 계류측 변(하단-전면 → 상단-전면)과 원지반의 교점 — I형 관 하단 자리. */
|
||
const groundOnFace = (points: OffsetPoint[]): OffsetPoint => {
|
||
const low = points[3];
|
||
const high = points[2];
|
||
let lo = 0;
|
||
let hi = 1;
|
||
for (let pass = 0; pass < 32; pass += 1) {
|
||
const mid = (lo + hi) / 2;
|
||
const point = {
|
||
offset: low.offset + (high.offset - low.offset) * mid,
|
||
elevation: low.elevation + (high.elevation - low.elevation) * mid,
|
||
};
|
||
if (point.elevation < groundAt(point.offset)) lo = mid;
|
||
else hi = mid;
|
||
}
|
||
const t = (lo + hi) / 2;
|
||
return {
|
||
offset: low.offset + (high.offset - low.offset) * t,
|
||
elevation: low.elevation + (high.elevation - low.elevation) * t,
|
||
};
|
||
};
|
||
const wallBase = wallBaseAt(wallHeight);
|
||
const iWallBottom = shape === "I" ? iBottomAt(wallBase) : defaultBottom;
|
||
const parts: BasinLayout["parts"] = [
|
||
{ kind: "wall", points: wallOf(wallBase, outward, iWallBottom) },
|
||
];
|
||
const iWallPoints = parts[0].points;
|
||
/** 벽 계류측 변 위에서 그 표고에 해당하는 점 — 관 하단을 벽면을 따라 내릴 때 쓴다. */
|
||
const pointOnFaceAt = (elevation: number): OffsetPoint => {
|
||
const low = iWallPoints[3];
|
||
const high = iWallPoints[2];
|
||
const span = high.elevation - low.elevation;
|
||
const t = span > 1e-9 ? Math.min(Math.max((elevation - low.elevation) / span, 0), 1) : 0;
|
||
return {
|
||
offset: low.offset + (high.offset - low.offset) * t,
|
||
elevation: low.elevation + span * t,
|
||
};
|
||
};
|
||
/**
|
||
* I형 관 하단 자리 = 벽 계류측 변과 원지반의 교점. 다만 **관이 벽을 관통해야** 하므로
|
||
* (ㄴ·ㄷ형은 바닥판 위에서 이미 물린다) 벽 상단에서 관경+여유만큼 아래로 제한한다.
|
||
* 원지반이 벽 상단보다 높은 자리에서 제한이 없으면 관이 벽 위로 떠 벽과 만나지 않았다
|
||
* (2026-09-03 사용자 지적: 「측벽과 배관의 형상이 교차하고 있어야 함」).
|
||
*/
|
||
const iGroundIntersection = (): OffsetPoint => {
|
||
const hit = groundOnFace(iWallPoints);
|
||
const invertCeiling = anchor.elevation + wallHeight - (diameterM + REVET_FREEBOARD_M);
|
||
return hit.elevation <= invertCeiling ? hit : pointOnFaceAt(invertCeiling);
|
||
};
|
||
// ㄴ형 = I형 + 바닥판. 바닥은 벽 바깥(계류측) 변에 맞대고 그 너머로 뻗는다
|
||
// (2026-08-20 확정). 벽이 기울어 있어 안쪽 변도 벽 바깥면 선을 그대로 따른다.
|
||
const wallOuterAt = (elevation: number): number => {
|
||
const bottom = anchor.elevation - floorThickness;
|
||
const t = (elevation - bottom) / (wallHeight + floorThickness);
|
||
return wallBase + outward * memberT - outward * lean * wallHeight * t;
|
||
};
|
||
const floorTopInner = wallOuterAt(anchor.elevation);
|
||
const floorBottomInner = wallOuterAt(anchor.elevation - floorThickness);
|
||
const floorOuter = floorTopInner + outward * (adjust.innerWidthM + memberT);
|
||
const floorTaper = lean * floorThickness;
|
||
if (shape !== "I") {
|
||
parts.push({
|
||
kind: "floor",
|
||
points: [
|
||
{ offset: floorTopInner, elevation: anchor.elevation },
|
||
{ offset: floorOuter, elevation: anchor.elevation },
|
||
{
|
||
offset: floorOuter - outward * floorTaper,
|
||
elevation: anchor.elevation - floorThickness,
|
||
},
|
||
{ offset: floorBottomInner, elevation: anchor.elevation - floorThickness },
|
||
],
|
||
});
|
||
}
|
||
// ㄷ형 = ㄴ형 + 막음벽 — I형 벽을 **바닥부 중심 기준 수직 반전**한 자리·형상
|
||
// (2026-08-22 사용자 ① — 종전에는 노견 쪽에 세워 방향이 틀렸다).
|
||
const floorCenter = (floorTopInner + floorOuter) / 2;
|
||
const mirroredWallBase = 2 * floorCenter - wallBase;
|
||
if (shape === "U") {
|
||
parts.push({ kind: "wall", points: wallOf(mirroredWallBase, -outward) });
|
||
}
|
||
// 구조물 계류측 끝(ㄴ형 = 바닥 바깥 끝 상단, ㄷ형 = 막음벽 계류측 상단 꼭짓점)과
|
||
// 원지반의 관계(2026-08-22 사용자 ①):
|
||
// · 원지반 안쪽에 박히면 → 표준단면 절토경사(1:n)로 **절토선**
|
||
// · 원지반 밖으로 나오면 → **성토(되메움)선** — 1:1.2로 지반까지.
|
||
let basinCut: BasinLayout["cutLine"] = null;
|
||
let fillStart: OffsetPoint | null = null;
|
||
if (shape !== "I") {
|
||
const refTop =
|
||
shape === "U"
|
||
? {
|
||
offset: mirroredWallBase + outward * lean * wallHeight,
|
||
elevation: anchor.elevation + wallHeight,
|
||
}
|
||
: { 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;
|
||
}
|
||
}
|
||
} else if (groundHere < refTop.elevation - 0.05) {
|
||
// 원지반 밖으로 나왔다 — 성토부선은 유출측과 같은 체계(1:1.2 + 5m 다단)로
|
||
// 본체가 그린다(2026-08-22 사용자 확정). 여기서는 시작점만 넘긴다.
|
||
fillStart = refTop;
|
||
}
|
||
}
|
||
return {
|
||
basin: {
|
||
shape,
|
||
parts,
|
||
// 라벨 자리 = I형 벽 하단(2026-08-20 사용자) — 그리기 쪽에서 아래로 내려 붙인다.
|
||
label: {
|
||
offset: wallBase + outward * (memberT / 2),
|
||
elevation: anchor.elevation - floorThickness,
|
||
},
|
||
reason,
|
||
cutLine: basinCut,
|
||
// I형 관 하단 되메움선만 여기 담긴다(applyBasinPipeFill). ㄴ·ㄷ형 성토는
|
||
// 성토부선 체계로 옮겼다.
|
||
fillLine: null,
|
||
},
|
||
// 관 끝선 = I형 벽의 안쪽(물받는 쪽 = 내공측) 변 복사(2026-08-22 사용자 확정).
|
||
// 벽 폴리곤의 하단-전면(3)~상단-전면(2) 꼭짓점을 그대로 쓴다 — I형 근입 연장도 반영.
|
||
endFace: {
|
||
base: iWallPoints[3],
|
||
direction: {
|
||
offset: iWallPoints[2].offset - iWallPoints[3].offset,
|
||
elevation: iWallPoints[2].elevation - iWallPoints[3].elevation,
|
||
},
|
||
},
|
||
pipeEnd:
|
||
shape === "I"
|
||
? iGroundIntersection()
|
||
: { offset: floorTopInner, elevation: anchor.elevation },
|
||
fillStart,
|
||
fillBottomElevation: anchor.elevation - floorThickness,
|
||
trimOffset: wallBase - outward * lean * wallHeight,
|
||
trimElevation: anchor.elevation + wallHeight,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 조정창 드롭다운 선택지 가용성(2026-08-22 사용자 — 상황에 안 맞는 선택지 숨김):
|
||
* · 기슭막이+배관: 자동 자리의 원지반이 토피 상한(노면−관경−토피)보다 높으면 배관이
|
||
* 원지반에 묻힐 수밖에 없다 → 숨김.
|
||
* · 집수정 ㄴ·ㄷ형: ㄴ형으로 가정해 세워 봤을 때 구조물이 원지반으로 밀고 들어가지
|
||
* 않으면(절토선이 안 생기면) → 숨김.
|
||
*/
|
||
export function inletChoiceAvailability(input: {
|
||
designAt: ((offset: number) => number) | null;
|
||
groundAt: (offset: number) => number;
|
||
edge: { offset_m: number; elevation_m: number };
|
||
outward: number;
|
||
limitOffset: number;
|
||
wallHeight: number;
|
||
invertCapM: number;
|
||
diameterM: number;
|
||
cutSlopeRatio: number;
|
||
}): { revetAllowed: boolean; basinLUAllowed: boolean; revetAutoOffset: number } {
|
||
const { designAt, groundAt, edge, outward, limitOffset, wallHeight, invertCapM } = input;
|
||
let revetAllowed = true;
|
||
// 기슭막이 자동 자리(노견 최소 지점 ?? 사면 끝) — 본체가 벽 배치에도 재사용한다.
|
||
let revetAutoOffset = edge.offset_m;
|
||
if (designAt) {
|
||
const baseAt = (offset: number): number => Math.min(groundAt(offset), invertCapM);
|
||
const toe = slopeToeOffset(designAt, groundAt, edge.offset_m, limitOffset);
|
||
revetAutoOffset =
|
||
minShoulderWallOffset(edge, outward, wallHeight, baseAt, toe, limitOffset) ?? toe;
|
||
revetAllowed = groundAt(revetAutoOffset) <= invertCapM + 0.02;
|
||
}
|
||
const probe = buildBasin({
|
||
anchor: { offset: edge.offset_m, elevation: edge.elevation_m - BASIN_INNER_HEIGHT_M },
|
||
outward,
|
||
shape: "L",
|
||
reason: "short",
|
||
diameterM: input.diameterM,
|
||
edge,
|
||
groundAt,
|
||
cutSlopeRatio: input.cutSlopeRatio,
|
||
adjust: {
|
||
innerWidthM: BASIN_INNER_WIDTH_M,
|
||
innerHeightM: BASIN_INNER_HEIGHT_M,
|
||
lateralM: 0,
|
||
slopeM: 0,
|
||
},
|
||
});
|
||
return { revetAllowed, basinLUAllowed: probe.basin.cutLine != null, revetAutoOffset };
|
||
}
|
||
|
||
/**
|
||
* I형 집수정의 **관 하단 수평 되메움(성토)선**(2026-08-22 사용자). 관 유입 하단
|
||
* 꼭짓점이 원지반 위에 뜨면 그 표고 그대로 계류측으로 나가 원지반과 만나는 점까지
|
||
* 잇고, 그 아래를 성토로 본다. 지반 교점 절단 방식은 보고 후 철회.
|
||
*/
|
||
export function applyBasinPipeFill(
|
||
basin: BasinLayout,
|
||
corner: OffsetPoint,
|
||
outward: number,
|
||
groundAt: (offset: number) => number,
|
||
): void {
|
||
// 되메움선 물매 3도(2026-08-22 사용자) — 유입측이라 계류 쪽이 높다(물이 관으로).
|
||
if (basin.shape !== "I" || corner.elevation <= groundAt(corner.offset) + 0.02) return;
|
||
let previousGap = corner.elevation - groundAt(corner.offset); // 선 − 지반
|
||
for (let run = 0.05; run <= 30; run += 0.05) {
|
||
const probe = corner.offset + outward * run;
|
||
const elevation = corner.elevation + PIPE_CONNECT_GRADE * run;
|
||
const gap = elevation - groundAt(probe);
|
||
if (gap <= 0) {
|
||
// 끝점은 보간 교차점 — 격자점이면 지반을 미세하게 벗어난다(2026-08-22 사용자).
|
||
const fraction = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
|
||
const runCross = run - 0.05 * (1 - fraction);
|
||
basin.fillLine = {
|
||
from: corner,
|
||
to: {
|
||
offset: corner.offset + outward * runCross,
|
||
elevation: corner.elevation + PIPE_CONNECT_GRADE * runCross,
|
||
},
|
||
};
|
||
return;
|
||
}
|
||
previousGap = gap;
|
||
}
|
||
}
|
||
|
||
/** 집수정 조립 결과 — 본체가 트림·접근선·관 접속에 그대로 꽂아 쓴다. */
|
||
export interface BasinAttachResult extends BasinBuildResult {
|
||
/** 성토 사면선 폴리라인(노견 → 수평 연장 → 벽 상단). 없으면 트림만. */
|
||
approach: { points: OffsetPoint[] } | null;
|
||
}
|
||
|
||
/** buildBasin + 접근선까지 한 번에 — 본체(Geom) 700줄 제한용 묶음. */
|
||
export function attachBasin(input: BasinBuildInput): BasinAttachResult {
|
||
const built = buildBasin(input);
|
||
const points = basinApproachPoints(input.edge, input.outward, input.adjust, {
|
||
offset: built.trimOffset,
|
||
elevation: built.trimElevation,
|
||
});
|
||
return { ...built, approach: points ? { points } : null };
|
||
}
|
||
|
||
/**
|
||
* I형 집수정 **이동 한계**(2026-08-22 사용자 확정) — I형은 관을 감싸는 구조라 옮기면
|
||
* 관 시작점이 따라가 **관 길이(m 규격)가 바뀐다**. 길이가 바뀌는 지점 직전이 최대다.
|
||
* 요청값에서 0.1씩(상하 먼저, 다음 좌우) 되돌려 기준 길이와 같아지는 자리를 찾는다.
|
||
* `probe`는 같은 단면을 다시 계산하는 함수(본체가 재귀로 물려 준다).
|
||
*/
|
||
export function clampBasinMove(
|
||
adjust: BasinAdjust,
|
||
probe: (candidate: BasinAdjust) => { shape: BasinShape | null; lengthM: number } | null,
|
||
): BasinAdjust {
|
||
if (adjust.lateralM <= 1e-9 && adjust.slopeM <= 1e-9) return adjust;
|
||
const base = probe({ ...adjust, lateralM: 0, slopeM: 0 });
|
||
if (base?.shape !== "I") return adjust;
|
||
const round1 = (value: number): number => Math.round(value * 10) / 10;
|
||
let lateralM = adjust.lateralM;
|
||
let slopeM = adjust.slopeM;
|
||
for (let pass = 0; pass < 400 && (lateralM > 1e-9 || slopeM > 1e-9); pass += 1) {
|
||
if (probe({ ...adjust, lateralM, slopeM })?.lengthM === base.lengthM) break;
|
||
if (slopeM > 1e-9) slopeM = Math.max(0, round1(slopeM - 0.1));
|
||
else lateralM = Math.max(0, round1(lateralM - 0.1));
|
||
}
|
||
return { ...adjust, lateralM, slopeM };
|
||
}
|