- 유입 · 유출 구조 「흙막이」면 배관 흙막이와 같은 세트(관 숨김 · body)를 같은 측점 culvert 자리에 함께 얹음(파이썬 _guard_set ↔ TS guardSet 짝 · 거울 시험) - 자동 자리 = 벽 전면이 관(세월교) · 본체(박스) 끝 · 좌우 x 는 그 자리 기준(본체를 늘리면 벽도 따라감) · 상하 d 는 배관과 같음 - 연동 옆 측점 · 3D 연동 해제 자리는 본체 기준 x 를 절대값으로 넘겨 받음 · 3D 만료 해시에 흙막이 값 포함 - 설계선 트림 = 벽 선 쪽은 흙막이 · 안 선 쪽은 본체 · 벽 선 쪽 본체 성토선은 걷음(두 줄 방지) · 관 연장은 안 실음 - 좌측 폼 → 캐시: 세트 다시 짓기 · 높이 · 형태 · 추가 흙막이 상자 수 = 유출 다단 수 - [연동][경사] 회색 풀림(옆 측점에 벽이 서면 연동 · 구간 있으면 경사) — 배관과 같은 규칙 - 시험: test_73_4_body_guard(벽 전면 = 본체 끝 · 박스 +1 m 따라감 · 연동 옆 측점 벽 · 서버 Node 면적 = 화면) · 거울 시험 흙막이 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
151 lines
6.2 KiB
TypeScript
151 lines
6.2 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_Body_Guard.ts
|
|
* 세월교 · 박스 유입 · 유출 흙막이(73-4) — 관보호공 흙막이와 **같은 벽 · 같은 자리 규칙**.
|
|
*
|
|
* 세트는 배관 흙막이와 같은 `section.culvert`(관 숨김 · `body`)로 얹혀 그림 · 연동 · 경사 · 3D ·
|
|
* 면적 길을 그대로 탐. 다른 점은 자동 자리 하나 — 벽 전면이 관(세월교) · 본체(박스) 끝에 옴
|
|
* (배관 벽 전면이 관 끝인 것과 같음). 좌우 x 는 그 자리 기준 · 상하 d 는 배관과 같은 절대값.
|
|
* 본체가 이 측점에 없으면(연동 옆 측점) 배관 규칙 그대로 — 소유 측점 값을 절대값으로 넘겨 받음.
|
|
* ========================================================================== */
|
|
|
|
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
|
|
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
|
|
import type { BoxLayout } from "./B06_Section_UI_Cross_Box_Geom";
|
|
import { FILL_SLOPE_RATIO_MIN } from "./B06_Section_UI_Cross_Culvert_Const";
|
|
import { outletPipeEnd } from "./B06_Section_UI_Cross_Culvert_Tiers";
|
|
import type {
|
|
CulvertDesignTrim,
|
|
CulvertLayout,
|
|
OffsetPoint,
|
|
WallAdjust,
|
|
WallLayout,
|
|
} from "./B06_Section_UI_Cross_Culvert_Types";
|
|
import { computeFordLayout, DEFAULT_FORD_WALL_ADJUST } from "./B06_Section_UI_Cross_Ford_Geom";
|
|
import type { FordLayout } from "./B06_Section_UI_Cross_Ford_Geom";
|
|
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
|
|
|
|
type Role = "inlet" | "outlet";
|
|
|
|
/** 본체 끝(관 invert · 박스 저판 밑) + 본체 설계선 트림 — 본체가 이 측점에 없으면 null. */
|
|
export interface BodyGuardBase {
|
|
ends: Record<Role, OffsetPoint | null>;
|
|
trim: CulvertDesignTrim;
|
|
}
|
|
|
|
export function bodyGuardBase(
|
|
section: CrossSection,
|
|
samples: SectionSample[],
|
|
): BodyGuardBase | null {
|
|
const body = section.culvert?.body;
|
|
const adjust = section.design;
|
|
if (body === "box" && section.box) {
|
|
const layout = computeBoxLayout(section, samples, {
|
|
left: { ...DEFAULT_BOX_SIDE_ADJUST, ...(adjust?.box_adjust?.left ?? {}) },
|
|
right: { ...DEFAULT_BOX_SIDE_ADJUST, ...(adjust?.box_adjust?.right ?? {}) },
|
|
});
|
|
if (!layout) return null;
|
|
const inletOnLeft = (section.uphill_side ?? "left") === "left";
|
|
const endOf = (role: Role): OffsetPoint | null => {
|
|
const side = layout.sides.find(
|
|
(s) => (s.role === "left") === ((role === "inlet") === inletOnLeft),
|
|
);
|
|
return side ? { offset: side.offset, elevation: side.bottomElevation } : null;
|
|
};
|
|
return { ends: { inlet: endOf("inlet"), outlet: endOf("outlet") }, trim: layout.designTrim };
|
|
}
|
|
if (body === "ford" && section.ford) {
|
|
const layout = computeFordLayout(section, samples, {
|
|
inlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(adjust?.ford_adjust?.inlet ?? {}) },
|
|
outlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(adjust?.ford_adjust?.outlet ?? {}) },
|
|
});
|
|
if (!layout) return null;
|
|
const endOf = (role: Role): OffsetPoint | null =>
|
|
layout.sides.find((s) => s.role === role)?.pipeEnd ?? null;
|
|
return { ends: { inlet: endOf("inlet"), outlet: endOf("outlet") }, trim: layout.designTrim };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 벽 전면을 본체 끝에 맞춘 배관 벽 4축(x · d) — `placePipeWall` 역산.
|
|
* 바닥 기준(invert) = 본체 끝 표고 · 상단 = 노견 − d/1.2 · 자리 = 0점 + (x + d).
|
|
*/
|
|
export function bodyGuardAxes(
|
|
end: OffsetPoint,
|
|
wall: {
|
|
autoOffset: number;
|
|
outward: number;
|
|
height: number;
|
|
edgeElevation: number;
|
|
lean: number;
|
|
thickness: number;
|
|
},
|
|
): { x: number; d: number } {
|
|
const d = FILL_SLOPE_RATIO_MIN * (wall.edgeElevation - wall.height - end.elevation);
|
|
// 전면 위치는 자리(offset)에 평행이동만 함 — 0 에 세워 재고 옮김(노면 멈춤도 같게).
|
|
const probe = buildRevetWallGeometry({
|
|
anchor: { offset: 0, elevation: end.elevation },
|
|
outward: wall.outward,
|
|
height: wall.height,
|
|
material: "dry",
|
|
role: "outlet",
|
|
thickness: wall.thickness,
|
|
lean: wall.lean,
|
|
roadElevation: wall.edgeElevation,
|
|
});
|
|
const anchorOffset = end.offset - outletPipeEnd(probe).offset;
|
|
return { x: (anchorOffset - wall.autoOffset) * wall.outward - d, d };
|
|
}
|
|
|
|
/** 벽이 선 쪽은 흙막이 트림 · 안 선 쪽은 본체 트림(73-4). 벽이 하나도 없으면 null. */
|
|
export function mergeGuardTrim(
|
|
guard: CulvertDesignTrim,
|
|
body: CulvertDesignTrim,
|
|
walls: readonly WallLayout[],
|
|
): CulvertDesignTrim | null {
|
|
if (!walls.length) return null;
|
|
const max = walls.some((w) => w.outward > 0) ? guard : body;
|
|
const min = walls.some((w) => w.outward < 0) ? guard : body;
|
|
return {
|
|
minOffset: min.minOffset,
|
|
maxOffset: max.maxOffset,
|
|
minElevation: min.minElevation,
|
|
maxElevation: max.maxElevation,
|
|
minSlope: min.minSlope,
|
|
maxSlope: max.maxSlope,
|
|
};
|
|
}
|
|
|
|
/** 소유 측점 적용값(x = 본체 기준) → 옆 측점이 받을 절대값 — 옆 측점엔 본체가 없음. */
|
|
export function bodyAbsolute(layout: CulvertLayout): CulvertLayout["revetShift"] {
|
|
const shift = layout.revetShift;
|
|
const bodyX = layout.bodyX;
|
|
if (!bodyX) return shift;
|
|
const plus = (adjust: WallAdjust, dx: number): WallAdjust => ({ ...adjust, x: adjust.x + dx });
|
|
return {
|
|
...shift,
|
|
inlet: plus(shift.inlet, bodyX.inlet),
|
|
outlet: plus(shift.outlet, bodyX.outlet),
|
|
};
|
|
}
|
|
|
|
/** 흙막이가 선 쪽의 본체 성토선을 걷음 — 그 쪽 성토선은 흙막이 벽이 냄(두 줄 방지). */
|
|
export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
|
|
layout: T | null,
|
|
culvert: CulvertLayout | null,
|
|
): T | null {
|
|
if (!layout || !culvert?.culvert.body) return layout;
|
|
const guarded = (outward: number): boolean => culvert.walls.some((w) => w.outward === outward);
|
|
if ("box" in layout && "topSlab" in layout) {
|
|
const sides = layout.sides.map((side) =>
|
|
guarded(side.role === "left" ? 1 : -1) ? { ...side, fillLine: [] } : side,
|
|
);
|
|
return { ...layout, sides };
|
|
}
|
|
const ford = layout as FordLayout;
|
|
const sides = ford.sides.map((side) =>
|
|
guarded(side.outward) ? { ...side, fillSegments: [] } : side,
|
|
);
|
|
return { ...ford, sides } as T;
|
|
}
|