- 지반 교점 절단(직전 커밋)은 사용자 확인 후 철회 — 관 끝은 벽 안쪽 변 절단으로 복귀. - 대신 관 유입 하단 꼭짓점이 원지반 위에 뜨면 그 표고 그대로 계류측으로 **수평 되메움(성토)선**을 그어 원지반과 연결(applyBasinPipeFill — Basin 분리, Geom 700줄 유지). - 검증: 13+15.7 I형 실화면 — 성토선 수평·관 꼭짓점 부착 0.4px, 스크린샷. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
274 lines
12 KiB
TypeScript
274 lines
12 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 {
|
|
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,
|
|
BasinShape,
|
|
EndFace,
|
|
OffsetPoint,
|
|
} from "./B06_Section_UI_Cross_Culvert_Types";
|
|
|
|
/** 집수정 구성 입력 — 기하 본체가 자기 상태를 명시적으로 물려 준다. */
|
|
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;
|
|
}
|
|
|
|
/** 집수정 구성 결과 — 본체가 트림·관 끝단면·관 시작점에 나눠 꽂는다. */
|
|
export interface BasinBuildResult {
|
|
basin: BasinLayout;
|
|
/** 관 끝단 마감면 = I형 벽의 안쪽(물받는 쪽) 변 복사(2026-08-22 사용자 확정). */
|
|
endFace: EndFace;
|
|
/** 관 시작점(하단 기준) — ㄴ·ㄷ형은 측벽 안쪽 변×바닥 윗면 교차점, I형은 내공 1.0m. */
|
|
pipeEnd: OffsetPoint;
|
|
/** 성토 사면선 트림 경계(벽 상단 도로측 꼭짓점)와 그 표고. */
|
|
trimOffset: number;
|
|
trimElevation: number;
|
|
}
|
|
|
|
/** 실무 내공 폭(m) — 울진 돌집수정. I형 관 시작점(노견+1.0m) 계산에도 쓴다. */
|
|
export const BASIN_INNER_WIDTH_M = 1.0;
|
|
const INNER_WIDTH_M = BASIN_INNER_WIDTH_M;
|
|
|
|
/** 집수정 주변 성토(되메움) 표면 경사(도) — 2026-08-22 사용자 지정 "일단 5도". */
|
|
const BASIN_FILL_ANGLE_DEG = 5;
|
|
|
|
/** 집수정 단면을 만든다. 형태별 규칙은 파일 머리말 참조. */
|
|
export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
|
const { anchor, outward, shape, reason, diameterM, edge, groundAt, cutSlopeRatio } = input;
|
|
const memberT = BASIN_MEMBER_THICKNESS_M;
|
|
const floorThickness = memberT;
|
|
// 벽 높이: I형은 노견 접점까지 성장, ㄴ·ㄷ형은 내부 높이 1.2m 고정(임시값).
|
|
const wallHeight =
|
|
shape === "I"
|
|
? Math.max(diameterM + REVET_FREEBOARD_M, edge.elevation_m - anchor.elevation)
|
|
: BASIN_INNER_HEIGHT_M;
|
|
// 기움은 **수직 기준 반전** — 상단이 도로측(내공 쪽)으로 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 * REVET_LEAN_RATIO * 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 wallBase = anchor.offset + outward * (REVET_LEAN_RATIO * wallHeight);
|
|
// I형 벽 하단은 **원지반까지 묻는다**(2026-08-22 사용자 — 지반 아래 부재두께만큼
|
|
// 근입 후 배관을 둔다). ㄴ·ㄷ형은 바닥판이 원지반을 대체하므로 현행 유지.
|
|
const iWallBottom =
|
|
shape === "I"
|
|
? Math.min(anchor.elevation, groundAt(wallBase), groundAt(wallBase + outward * memberT)) -
|
|
floorThickness
|
|
: defaultBottom;
|
|
const parts: BasinLayout["parts"] = [
|
|
{ kind: "wall", points: wallOf(wallBase, outward, iWallBottom) },
|
|
];
|
|
const iWallPoints = parts[0].points;
|
|
// ㄴ형 = 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 * REVET_LEAN_RATIO * wallHeight * t;
|
|
};
|
|
const floorTopInner = wallOuterAt(anchor.elevation);
|
|
const floorBottomInner = wallOuterAt(anchor.elevation - floorThickness);
|
|
const floorOuter = floorTopInner + outward * (INNER_WIDTH_M + memberT);
|
|
const floorTaper = REVET_LEAN_RATIO * 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)로 **절토선**
|
|
// · 원지반 밖으로 나오면 → **성토(되메움)선** — 경사 5도(임시값)로 지반까지.
|
|
let basinCut: BasinLayout["cutLine"] = null;
|
|
let basinFill: BasinLayout["cutLine"] = null;
|
|
if (shape !== "I") {
|
|
const refTop =
|
|
shape === "U"
|
|
? {
|
|
offset: mirroredWallBase + outward * REVET_LEAN_RATIO * 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) {
|
|
const dropPerM = Math.tan((BASIN_FILL_ANGLE_DEG * Math.PI) / 180);
|
|
for (let run = 0.05; run <= 30; run += 0.05) {
|
|
const probe = refTop.offset + outward * run;
|
|
const elevation = refTop.elevation - dropPerM * run;
|
|
if (elevation <= groundAt(probe)) {
|
|
basinFill = { from: refTop, to: { offset: probe, elevation } };
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
basin: {
|
|
shape,
|
|
parts,
|
|
// 라벨 자리 = I형 벽 하단(2026-08-20 사용자) — 그리기 쪽에서 아래로 내려 붙인다.
|
|
label: {
|
|
offset: wallBase + outward * (memberT / 2),
|
|
elevation: anchor.elevation - floorThickness,
|
|
},
|
|
reason,
|
|
cutLine: basinCut,
|
|
fillLine: basinFill,
|
|
},
|
|
// 관 끝선 = 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"
|
|
? { offset: anchor.offset + outward * INNER_WIDTH_M, elevation: anchor.elevation }
|
|
: { offset: floorTopInner, elevation: anchor.elevation },
|
|
trimOffset: wallBase - outward * REVET_LEAN_RATIO * 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,
|
|
});
|
|
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 {
|
|
if (basin.shape !== "I" || corner.elevation <= groundAt(corner.offset) + 0.02) return;
|
|
for (let run = 0.05; run <= 30; run += 0.05) {
|
|
const probe = corner.offset + outward * run;
|
|
if (groundAt(probe) >= corner.elevation) {
|
|
basin.fillLine = { from: corner, to: { offset: probe, elevation: corner.elevation } };
|
|
return;
|
|
}
|
|
}
|
|
}
|