Files
Aislo/B06_Section/B06_Section_UI_Cross_Body_Guard.ts
T

299 lines
13 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,
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";
/** 본체 끝 — 전면 자리(offset) · 윗면(top) · 바닥(bottom) · 관 invert(세월교 바닥판 상면) 표고 ·
* 흙막이 기본 순수 높이(79-7 · 79b · 80). */
export interface BodyEnd {
offset: number;
top: number;
bottom: number;
invert: number;
height: number;
}
/** 본체 끝(세월교 관 끝 · 박스 끝) + 본체 설계선 트림 — 본체가 이 측점에 없으면 null. */
export interface BodyGuardBase {
ends: Record<Role, BodyEnd | null>;
trim: CulvertDesignTrim;
}
/** `defaults` = 세월교 측벽 조작을 안 본 기본 자리(관 1 m 맞춤 기준 · 107-1). */
export function bodyGuardBase(
section: CrossSection,
samples: SectionSample[],
defaults = false,
): 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,
invert: 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 stored = defaults ? null : adjust?.ford_adjust;
const layout = computeFordLayout(section, samples, {
inlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(stored?.inlet ?? {}) },
outlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(stored?.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,
invert: side.slabTopElevation,
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;
},
): BodyAxes {
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;
// 배면 → 관 invert 높이 전면 — 세월교 관이 흙막이 안으로 드는 길이(80)
const front =
probe.points[2].offset + wall.outward * probe.lean * (probe.topJoint.elevation - end.invert);
const through = (front - probe.bottomBack.offset) * wall.outward;
return {
x: (anchorOffset - wall.autoOffset) * wall.outward - d,
d,
through,
outward: wall.outward,
};
}
export type BodyAxes = { x: number; d: number; through: number; outward: number };
/** 세월교 관은 1 m 단위(80) — 흙막이 전면까지 든 관 길이를 올림하고 모자란 만큼 흙막이를 바깥으로
* 미는 기본 x(유출 먼저 · 배관 정수 맞춤과 같은 쪽). 손으로 민 x 는 그 위에 1 m 걸음으로 얹힘.
* 맞춤 길이는 **기본 자리**(측벽 조작 0)로만 잼 — 한쪽 이동이 반대쪽 흙막이를 안 밈(107-1 ·
* 수량 L 은 그린 관 길이 올림 그대로). */
export function fordPipeFit(
section: CrossSection,
samples: SectionSample[],
culvert: { body?: string | null; inlet: { structure?: string }; outlet: { structure?: string } },
axesOf: (role: Role, base?: BodyGuardBase | null) => BodyAxes | null,
): [BodyAxes | null, BodyAxes | null] {
const axes: [BodyAxes | null, BodyAxes | null] = [axesOf("inlet"), axesOf("outlet")];
if (culvert.body !== "ford") return axes;
const base = bodyGuardBase(section, samples, true);
if (!base) return axes;
const roles = ["inlet", "outlet"] as const;
const guarded = roles.map((role) => (culvert[role].structure ? axesOf(role, base) : null));
const tips = roles.map((role, i) => {
const end = base.ends[role];
const reach = guarded[i] ? guarded[i]!.outward * guarded[i]!.through : 0;
return end && { offset: end.offset + reach, z: end.invert };
});
if (!tips[0] || !tips[1]) return axes;
// ponytail: 관 기울기는 작아 바깥 수평 이동으로 길이를 채움
const length = Math.hypot(tips[0].offset - tips[1].offset, tips[0].z - tips[1].z);
const gap = Math.ceil(length - 1e-6) - length;
const at = guarded[1] ? 1 : 0;
const fit = axes[at];
if (!guarded[at] || !fit || gap < 1e-6) return axes;
axes[at] = { ...fit, x: fit.x + gap };
return axes;
}
/** 벽이 선 쪽은 흙막이 트림 · 안 선 쪽은 본체 트림(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);
const mid = layout.label.at.offset;
const reach = (
points: OffsetPoint[],
outward: number,
offset: (p: OffsetPoint) => number,
): OffsetPoint[] =>
points.map((p) => ((p.offset - mid) * outward > 0 ? { ...p, offset: offset(p) } : p));
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,
);
// 박스 본체가 흙막이 배면까지 따라감 — 흙막이를 바깥으로 민 만큼만(80)
let { topSlab, bottomSlab, barrel } = layout;
for (const side of layout.sides) {
const outward = side.role === "left" ? 1 : -1;
const wall = culvert.walls.find((w) => w.role !== "extra" && w.outward === outward);
const to = wall?.bottomBack.offset ?? side.offset;
if ((to - side.offset) * outward < 1e-6) continue;
[topSlab, bottomSlab, barrel] = [topSlab, bottomSlab, barrel].map((p) =>
reach(p, outward, () => to),
);
}
const run =
Math.max(...topSlab.map((p) => p.offset)) - Math.min(...topSlab.map((p) => p.offset));
return { ...layout, sides, topSlab, bottomSlab, barrel, bodyLengthM: run };
}
const ford = layout as FordLayout;
const sides = ford.sides.map((side) =>
guarded(side.outward) ? { ...side, parts: [], fillSegments: [], cutLine: null } : side,
);
// 관 · 바닥판이 흙막이까지 따라감 — 관 끝 꼭짓점 = 그 높이의 흙막이 전면(사면 이용 · 89-1) ·
// 아래 꼭짓점(invert)은 종전 자리라 1 m 맞춤 그대로 · 바닥판 끝 = 배면(80)
let { pipe, slabBridge } = ford;
for (const side of ford.sides) {
const wall = culvert.walls.find((w) => w.role === side.role && w.outward === side.outward);
if (!wall) continue;
const faceAt = (p: OffsetPoint): number =>
wall.points[2].offset + wall.outward * wall.lean * (wall.topJoint.elevation - p.elevation);
pipe = {
outer: reach(pipe.outer, side.outward, faceAt),
inner: reach(pipe.inner, side.outward, faceAt),
};
slabBridge = reach(slabBridge, side.outward, () => wall.bottomBack.offset);
}
if (pipe === ford.pipe) return { ...ford, sides } as T;
const [right, left] = pipe.outer.slice(2);
const lengthM = Math.hypot(left.offset - right.offset, left.elevation - right.elevation);
const slabOffsets = [
...slabBridge,
...sides.flatMap((s) => s.parts.flatMap((p) => p.points)),
].map((p) => p.offset);
return {
...ford,
sides,
pipe,
slabBridge,
slabLengthM: Math.max(...slabOffsets) - Math.min(...slabOffsets),
label: { ...ford.label, text: `${ford.label.text} L${Math.ceil(lengthM - 1e-6)}m` },
} as T;
}
/** 세월교 · 박스 유입 흙막이 수평공간 시작 = 본체 바닥(세월교 바닥판 · 박스 저판 상면) 높이의
* 흙막이 전면 자리(89-3). 본체가 이 측점에 없으면 null(배관 규칙). */
export function guardFloorStart(
section: CrossSection,
base: BodyGuardBase | null,
wall: WallLayout,
): OffsetPoint | null {
const end = base?.ends.inlet;
if (!end) return null;
const slab = section.culvert?.body === "box" ? (section.box?.slab_thickness_m ?? 0) : 0;
const floor = end.invert + slab;
return {
offset: wall.points[2].offset + wall.outward * wall.lean * (wall.topJoint.elevation - floor),
elevation: floor,
};
}