Files
Aislo/B06_Section/B06_Section_UI_Cross_Body_Guard.ts
T
eomsangdonandClaude Opus 5.5 77e8aa2709 fix(B06): 박스 흙막이 기본 높이 = 박스 높이 값 · 저장 x 벽 종전 자리 그대로 · 흙막이 조정창 제목 · 박스 날개벽 툴팁(79b)
- 박스 흙막이 높이 빈 칸 = 박스 높이 값(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
2026-10-08 00:26:10 +09:00

183 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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;
}