- 박스 흙막이 높이 빈 칸 = 박스 높이 값(2 m 박스 → 2 m) · 윗면 = 본체 윗면 · 바닥 = 윗면 − 높이 · 세월교는 그대로 - x 원점(사면 경사길이 4 m 지점) 되돌림 — 손대지 않은 벽(x · d 빈 칸)만 성토사면 제일 위 · 손으로 x 를 넣은 독립 기슭막이 · 옹벽 · 관 보호공은 종전 자리 - 세월교 · 박스 흙막이 조정창 제목 「흙막이(유입 · 유출)」 · 박스 에이프런 툴팁 유입 · 유출 날개벽 이름 바로잡음 - 시험: test_79_guard_shape — 박스 높이 값 · 저장 x 있는 관 보호공 · 독립 기슭막이 = 종전 자리 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
183 lines
7.5 KiB
TypeScript
183 lines
7.5 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Cross_Body_Guard.ts
|
||
* 세월교 · 박스 유입 · 유출 흙막이(73-4) — 관보호공 흙막이와 **같은 벽 · 같은 자리 규칙**.
|
||
*
|
||
* 세트는 배관 흙막이와 같은 `section.culvert`(관 숨김 · `body`)로 얹혀 그림 · 연동 · 경사 · 3D ·
|
||
* 면적 길을 그대로 탐. 다른 점은 자동 자리 하나 — 벽 **배면(수직)이 본체 끝**에 붙어 벽 몸이 본체
|
||
* 바깥에 섬 · 벽 윗면 = 본체 윗면(세월교 상판 · 박스 상판) · 높이 기본 = 세월교 본체 끝 높이 ·
|
||
* 박스 높이 값(2 m 박스 → 2 m · 79b · 바닥 = 윗면 − 높이)(79-7 · 그림
|
||
* `tmp/79_세월교_기본위치.png`). 좌우 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 type {
|
||
CulvertDesignTrim,
|
||
CulvertLayout,
|
||
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";
|
||
|
||
/** 본체 끝 — 전면 자리(offset) · 윗면(top) · 바닥(bottom) 표고 · 흙막이 기본 순수 높이(79-7 · 79b). */
|
||
export interface BodyEnd {
|
||
offset: number;
|
||
top: number;
|
||
bottom: number;
|
||
height: number;
|
||
}
|
||
|
||
/** 본체 끝(세월교 관 끝 · 박스 끝) + 본체 설계선 트림 — 본체가 이 측점에 없으면 null. */
|
||
export interface BodyGuardBase {
|
||
ends: Record<Role, BodyEnd | 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): BodyEnd | null => {
|
||
const side = layout.sides.find(
|
||
(s) => (s.role === "left") === ((role === "inlet") === inletOnLeft),
|
||
);
|
||
return side
|
||
? {
|
||
offset: side.offset,
|
||
top: side.topElevation,
|
||
bottom: side.bottomElevation,
|
||
// 박스 높이 값 그대로(사용자 원문 2 m → 2 m · 79b)
|
||
height: layout.box.inner_height_m,
|
||
}
|
||
: 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): BodyEnd | null => {
|
||
const side = layout.sides.find((s) => s.role === role);
|
||
return side
|
||
? {
|
||
offset: side.pipeEnd.offset,
|
||
top: side.top.elevation,
|
||
bottom: side.slabTopElevation - layout.ford.slab_thickness_m,
|
||
height: side.top.elevation - (side.slabTopElevation - layout.ford.slab_thickness_m),
|
||
}
|
||
: null;
|
||
};
|
||
return { ends: { inlet: endOf("inlet"), outlet: endOf("outlet") }, trim: layout.designTrim };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 흙막이 기본 순수 높이(근입 포함) — 세월교 = 본체 끝 높이 · 박스 = 박스 높이 값(79-7 · 79b). */
|
||
export const bodyGuardHeight = (end: BodyEnd): number => end.height;
|
||
|
||
/**
|
||
* 벽 배면을 본체 끝에 붙인 배관 벽 4축(x · d) — `placePipeWall` 역산.
|
||
* 상단 = 본체 윗면 = 노견 − d/1.2 · 바닥 기준(invert) = 상단 − 높이 · 자리 = 0점 + (x + d).
|
||
*/
|
||
export function bodyGuardAxes(
|
||
end: BodyEnd,
|
||
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 - end.top);
|
||
// 배면 위치는 자리(offset)에 평행이동만 함 — 0 에 세워 재고 옮김(노면 멈춤도 같게).
|
||
const probe = buildRevetWallGeometry({
|
||
anchor: { offset: 0, elevation: end.top - wall.height },
|
||
outward: wall.outward,
|
||
height: wall.height,
|
||
material: "dry",
|
||
role: "outlet",
|
||
thickness: wall.thickness,
|
||
lean: wall.lean,
|
||
roadElevation: wall.edgeElevation,
|
||
});
|
||
const anchorOffset = end.offset - probe.bottomBack.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),
|
||
};
|
||
}
|
||
|
||
/** 흙막이가 선 쪽의 본체 성토 · 절토선을 걷음 — 그 쪽 선은 흙막이 벽이 냄(두 줄 방지).
|
||
* 세월교는 그 쪽 ㄴ형 측벽도 걷음 — 흙막이가 측벽 자리를 대신함(79-7). */
|
||
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: [], fillSegments: [], cutLine: null }
|
||
: side,
|
||
);
|
||
return { ...layout, sides };
|
||
}
|
||
const ford = layout as FordLayout;
|
||
const sides = ford.sides.map((side) =>
|
||
guarded(side.outward) ? { ...side, parts: [], fillSegments: [], cutLine: null } : side,
|
||
);
|
||
return { ...ford, sides } as T;
|
||
}
|