Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -37,9 +37,9 @@ export function structureHashParts(section: CrossSection): Array<string | number
|
||||
);
|
||||
}
|
||||
const box = section.box;
|
||||
// 세월교 · 박스에 흙막이(`culvert`)가 함께 서면 그 값도 이어 붙임(73-4) — 일찍 돌려주지 않음.
|
||||
if (box) {
|
||||
return [
|
||||
...extras,
|
||||
extras.push(
|
||||
"bx",
|
||||
box.inner_width_m,
|
||||
box.inner_height_m,
|
||||
@@ -54,7 +54,7 @@ export function structureHashParts(section: CrossSection): Array<string | number
|
||||
const value = section.design?.box_adjust?.[side];
|
||||
return value ? `${side}:${value.lengthM},${value.riseM}` : "";
|
||||
}),
|
||||
];
|
||||
);
|
||||
}
|
||||
const ford = section.ford;
|
||||
if (ford) {
|
||||
@@ -63,8 +63,7 @@ export function structureHashParts(section: CrossSection): Array<string | number
|
||||
const value = adjust?.[role];
|
||||
return value ? `${value.heightM ?? ""},${value.lateralM},${value.slopeM}` : "";
|
||||
};
|
||||
return [
|
||||
...extras,
|
||||
extras.push(
|
||||
"fd",
|
||||
ford.diameter_m,
|
||||
ford.pipe_count,
|
||||
@@ -73,7 +72,7 @@ export function structureHashParts(section: CrossSection): Array<string | number
|
||||
ford.wing_out.slab_extend_m,
|
||||
wall("inlet"),
|
||||
wall("outlet"),
|
||||
];
|
||||
);
|
||||
}
|
||||
const culvert = section.culvert;
|
||||
if (!culvert) return extras;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import type { CulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
||||
import { computeCulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
||||
import { bodyAbsolute } from "../B06_Section/B06_Section_UI_Cross_Body_Guard";
|
||||
import type { WallAdjust } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
|
||||
import { ZERO_ADJUST } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
@@ -136,7 +137,17 @@ function detachedAdjusts(
|
||||
owner: CrossSection,
|
||||
section: CrossSection,
|
||||
): ReturnType<typeof storedAdjusts> {
|
||||
const base = storedAdjusts(owner);
|
||||
// 세월교 · 박스 흙막이 — 본체 기준 x 를 절대값으로(옆 측점엔 본체가 없음 · 횡단도와 같음 · 73-4)
|
||||
const ownerBody = owner.culvert?.body
|
||||
? computeCulvertLayout(
|
||||
owner,
|
||||
owner.samples,
|
||||
storedAdjusts(owner),
|
||||
"revet",
|
||||
owner.design?.basin_adjust,
|
||||
)
|
||||
: null;
|
||||
const base = ownerBody ? bodyAbsolute(ownerBody) : storedAdjusts(owner);
|
||||
const mine = section.design?.revet_adjust;
|
||||
const counts = owner.design?.extra_wall_counts;
|
||||
const pick = (role: string, fallback?: WallAdjust): WallAdjust =>
|
||||
|
||||
@@ -244,6 +244,8 @@ export interface CulvertSet {
|
||||
/** 기초 축 — "기초유" | "기초버림"(실무 정본 탭 제목). **비어 있으면 터파기를 안 그린다**
|
||||
* — 고르기 전에는 근거가 없다(2026-09-09 사용자 확정 ④). */
|
||||
foundation?: string | null;
|
||||
/** 세월교 · 박스 흙막이(73-4) — 자리 기준 본체. 있으면 벽 자동 자리 = 관(세월교) · 본체(박스) 끝. */
|
||||
body?: "box" | "ford";
|
||||
}
|
||||
|
||||
/** 세월교 날개벽 한쪽 — 횡단면엔 안 보이고 바닥판 연장량만 넘긴다. */
|
||||
|
||||
@@ -315,6 +315,36 @@ def _revet_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
# 세월교 · 박스 유입 · 유출 구조 칸(73-4) — 「흙막이」면 배관 흙막이와 같은 세트를 함께 얹음.
|
||||
_GUARD_KEYS = {"box": ("wing_in", "wing_out"), "ford": ("inlet_type", "outlet_type")}
|
||||
|
||||
|
||||
def _guard_set(values: dict[str, Any], body: str, diameter_m: float) -> dict[str, Any] | None:
|
||||
"""세월교 · 박스 흙막이 — 독립 기슭막이와 같은 세트 모양(관 숨김) · 자리 = 관 · 본체 끝(73-4).
|
||||
|
||||
같은 측점에 `culvert` 키로 함께 얹혀 배관 흙막이 길(그림 · 연동 · 경사 · 3D · 면적)을 그대로 탐.
|
||||
흙막이를 안 고른 쪽 = 구조 빈 값(안 그림) · 둘 다 아니면 None. TS 짝 `guardSet`.
|
||||
"""
|
||||
sides: dict[str, Any] = {}
|
||||
for role, key in zip(("inlet", "outlet"), _GUARD_KEYS[body]):
|
||||
guarded = str(values.get(key) or "").strip() == "흙막이"
|
||||
sides[role] = _revet_side(values, role) if guarded else {"role": role, "structure": ""}
|
||||
if not any(side["structure"] for side in sides.values()):
|
||||
return None
|
||||
return {
|
||||
"type": "pipe",
|
||||
"hidden_pipe": True,
|
||||
"body": body,
|
||||
"side": "양쪽",
|
||||
"tiers": 1,
|
||||
"foundation": None,
|
||||
"pipe_kind": None,
|
||||
"diameter_m": diameter_m,
|
||||
"min_cover_m": 0.0,
|
||||
**sides,
|
||||
}
|
||||
|
||||
|
||||
def _wing_spec(values: dict[str, Any], defaults: dict[str, Any], side: str) -> dict[str, Any]:
|
||||
"""날개벽 한쪽 제원 + 그 각도가 만드는 바닥판 연장량.
|
||||
|
||||
@@ -404,7 +434,7 @@ def _ford_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
("ford_height_m", "월류 높이", 0.0),
|
||||
],
|
||||
)
|
||||
return {
|
||||
spec = {
|
||||
"type": "ford",
|
||||
"pipe_kind": str(kind) if kind else None,
|
||||
"diameter_m": round((diameter_mm or 1000.0) / 1000.0, 3),
|
||||
@@ -423,6 +453,10 @@ def _ford_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
+ _wing_defaulted(values, wing_in, "in")
|
||||
+ _wing_defaulted(values, wing_out, "out"),
|
||||
}
|
||||
guard = _guard_set(values, "ford", spec["diameter_m"])
|
||||
if guard:
|
||||
spec["guard"] = guard
|
||||
return spec
|
||||
|
||||
|
||||
def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
@@ -474,7 +508,7 @@ def _box_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
slab = slab if slab and slab > 0 else FORD_SLAB_THICKNESS_M
|
||||
wing_in = _wing_spec(values, defaults, "in")
|
||||
wing_out = _wing_spec(values, defaults, "out")
|
||||
return {
|
||||
spec = {
|
||||
"type": "box",
|
||||
"inner_width_m": inner_width or 2.0,
|
||||
"inner_height_m": inner_height or 2.0,
|
||||
@@ -496,6 +530,15 @@ def _box_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
+ _wing_defaulted(values, wing_in, "in")
|
||||
+ _wing_defaulted(values, wing_out, "out"),
|
||||
}
|
||||
guard = _guard_set(values, "box", box_body_height(spec))
|
||||
if guard:
|
||||
spec["guard"] = guard
|
||||
return spec
|
||||
|
||||
|
||||
def box_body_height(box: dict[str, Any]) -> float:
|
||||
"""박스 본체 전체 높이(상판 + 내공 + 저판) — 흙막이 세트의 관경 자리(73-4)."""
|
||||
return round(box["top_thickness_m"] + box["inner_height_m"] + box["slab_thickness_m"], 3)
|
||||
|
||||
|
||||
def load_culvert_sets(project_root: Path) -> dict[float, dict[str, Any]]:
|
||||
@@ -578,6 +621,9 @@ def attach_culvert_sets(project_root: Path, cross_sections: list[dict[str, Any]]
|
||||
**spec,
|
||||
"chainage_m": pipe_chainage,
|
||||
}
|
||||
# 세월교 · 박스 흙막이(73-4) — 배관 흙막이와 같은 자리(`culvert`)에 함께 얹음.
|
||||
if spec.get("guard"):
|
||||
section["culvert"] = {**spec["guard"], "chainage_m": pipe_chainage}
|
||||
attached += 1
|
||||
break
|
||||
return attached
|
||||
|
||||
@@ -188,8 +188,10 @@ function areaRowOf(
|
||||
// ⇒ **가장 가까운 측점 하나**를 주인으로 본다. 거리로 자르지 않으므로 스냅 폭이
|
||||
// 바뀌어도 따라가고, 하나만 고르므로 두 번 세지도 않는다.
|
||||
const ownerChainage = pipeOwnerChainage(section, sections);
|
||||
// 세월교 · 박스 흙막이 세트(73-4)는 관이 아님 — 관 연장을 안 실음.
|
||||
const pipeOwner =
|
||||
!!section.culvert &&
|
||||
!section.culvert.body &&
|
||||
(ownerChainage === null ||
|
||||
Math.abs(ownerChainage - section.chainage_m) <= CHAINAGE_TOLERANCE_M);
|
||||
const pipeLengthM = pipeOwner ? layouts.culvert?.pipe?.lengthM : undefined;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/* =============================================================================
|
||||
* 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;
|
||||
}
|
||||
@@ -68,7 +68,8 @@ function facilityNotes(section: CrossSection): Array<[string, string]> {
|
||||
]);
|
||||
}
|
||||
const own = section.culvert?.hidden_pipe ? section.culvert : null;
|
||||
if (own && [own.inlet, own.outlet].some((side) => !side.revet_height_m)) {
|
||||
// 세월교 · 박스 흙막이(73-4)는 흙막이를 고른 쪽(구조 있음)만 봄
|
||||
if (own && [own.inlet, own.outlet].some((side) => side.structure && !side.revet_height_m)) {
|
||||
notes.push([
|
||||
"⚠ 기슭막이 높이 없음 — 근입만 그림",
|
||||
"독립 기슭막이 높이는 설계자 입력 — 계획홍수위 + 0.5~0.7m(사방기술교본 2-나:141 · 3-가:181) · 비우면 수량도 미확정 · B05 시설 칸에 적으면 그림",
|
||||
|
||||
@@ -22,10 +22,12 @@ import {
|
||||
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
|
||||
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
|
||||
import { bodyGuardAxes, bodyGuardBase, mergeGuardTrim } from "./B06_Section_UI_Cross_Body_Guard";
|
||||
import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type {
|
||||
BasinLayout,
|
||||
BasinAdjust,
|
||||
CulvertDesignTrim,
|
||||
CulvertLayout,
|
||||
EndFace,
|
||||
InletStructureChoice,
|
||||
@@ -152,6 +154,26 @@ export function computeCulvertLayout(
|
||||
};
|
||||
const inletWallSpec = pipeWallSpec(culvert.inlet, adjInlet);
|
||||
const outletWallSpec = pipeWallSpec(culvert.outlet, adjOutlet);
|
||||
// 세월교 · 박스 흙막이(73-4) — 자동 자리 = 본체 끝(x 0점 · d 기본값). 본체 없으면 배관 규칙.
|
||||
const body = culvert.body ? bodyGuardBase(section, groundSamples) : null;
|
||||
const bodyAxesOf = (role: "inlet" | "outlet", info: typeof inletInfo) => {
|
||||
const end = body?.ends[role];
|
||||
const spec = role === "inlet" ? inletWallSpec : outletWallSpec;
|
||||
if (!end) return null;
|
||||
return bodyGuardAxes(end, {
|
||||
autoOffset: slopeZeroAnchor(info.edge, info.outward, spec.height, spec.lean),
|
||||
outward: info.outward,
|
||||
height: spec.height,
|
||||
edgeElevation: info.edge.elevation_m,
|
||||
lean: spec.lean,
|
||||
thickness: REVET_THICKNESS_M,
|
||||
});
|
||||
};
|
||||
const inletBody = bodyAxesOf("inlet", inletInfo);
|
||||
const outletBody = bodyAxesOf("outlet", outletInfo);
|
||||
const onBody = (adjust: WallAdjust, axes: { x: number; d: number } | null): WallAdjust =>
|
||||
axes ? { ...adjust, x: axes.x + adjust.x, d: adjust.d ?? axes.d } : adjust;
|
||||
const minX = culvert.body ? Number.NEGATIVE_INFINITY : undefined;
|
||||
// 조정창 선택지 가용성 — 판정은 Basin이 맡는다.
|
||||
const inletOptions = inletChoiceAvailability({
|
||||
designAt,
|
||||
@@ -214,10 +236,11 @@ export function computeCulvertLayout(
|
||||
edgeElevation: inletInfo.edge.elevation_m,
|
||||
// 독립 기슭막이(관 숨김)는 관 토피·역경사 제약이 없다 — 유입도 노견까지 자유롭게 오르내린다.
|
||||
invertCap: culvert.hidden_pipe ? Number.POSITIVE_INFINITY : invertCap(inletInfo.edge),
|
||||
adjust: adjInlet,
|
||||
adjust: onBody(adjInlet, inletBody),
|
||||
defaultD: PIPE_WALL_DEFAULT_RUN_M,
|
||||
minX,
|
||||
});
|
||||
appliedAdjust.inlet.x = placed.x;
|
||||
appliedAdjust.inlet.x = placed.x - (inletBody?.x ?? 0);
|
||||
appliedAdjust.inlet.d = placed.d;
|
||||
wallVertical.inlet = {
|
||||
height: inletWallSpec.height,
|
||||
@@ -321,7 +344,8 @@ export function computeCulvertLayout(
|
||||
}
|
||||
// 구조 · 형태를 안 고른 배관 벽 = 그리지 않음(54-39 · 조정창 형태가 있으면 그 값)
|
||||
const pickedForm = (spec.role === "inlet" ? adjInlet : adjOutlet).m || spec.revet_form;
|
||||
if (!culvert.hidden_pipe && (spec.structure !== "기슭막이" || !pickedForm)) return null;
|
||||
if ((!culvert.hidden_pipe || culvert.body) && (spec.structure !== "기슭막이" || !pickedForm))
|
||||
return null;
|
||||
// 형상 = 공용 `buildRevetWallGeometry`(추가·사면안정 흙막이·옹벽과 한 벌). 높이는
|
||||
// vertical(계산용)이 들고 온다 — 형태별 높이 한계 없음, 윗면은 노면에서 멈춤.
|
||||
const height = vertical?.height ?? revetTargetHeight(diameter);
|
||||
@@ -401,10 +425,11 @@ export function computeCulvertLayout(
|
||||
edgeElevation: outletInfo.edge.elevation_m,
|
||||
// 독립 기슭막이는 양쪽 벽이 독립 — 유출을 유입 invert로 묶는 역경사 클램프를 뺀다.
|
||||
invertCap: culvert.hidden_pipe ? Number.POSITIVE_INFINITY : inlet.elevation,
|
||||
adjust: adjOutlet,
|
||||
adjust: onBody(adjOutlet, outletBody),
|
||||
defaultD: PIPE_WALL_DEFAULT_RUN_M,
|
||||
minX,
|
||||
});
|
||||
appliedAdjust.outlet.x = placed.x;
|
||||
appliedAdjust.outlet.x = placed.x - (outletBody?.x ?? 0);
|
||||
appliedAdjust.outlet.d = placed.d;
|
||||
wallVertical.outlet = {
|
||||
height: outletWallSpec.height,
|
||||
@@ -459,7 +484,7 @@ export function computeCulvertLayout(
|
||||
const outletThickness = REVET_THICKNESS_M;
|
||||
// 손으로 민 벽은 정수 맞춤이 **절대 옮기지 않는다**(2026-08-22) — 표기만 올림한다.
|
||||
// d = null(기본 자리)만 자동으로 본다(2026-08-23 — d는 0점 기준 절대값).
|
||||
const outletPinned = Math.abs(adjOutlet.x) > 1e-9 || adjOutlet.d != null;
|
||||
const outletPinned = !!outletBody || Math.abs(adjOutlet.x) > 1e-9 || adjOutlet.d != null;
|
||||
if (outletWall && !outletPinned) {
|
||||
const target = Math.ceil(cutLength(pipeCorners) - 1e-6);
|
||||
const rebuild = (): void => {
|
||||
@@ -608,6 +633,17 @@ export function computeCulvertLayout(
|
||||
extendTrimSlope(drawnFillPoints(extrasForArea), outletInfo.outward);
|
||||
extendTrimSlope(drawnFillPoints(basinExtrasForArea), inletInfo.outward);
|
||||
|
||||
const ownTrim: CulvertDesignTrim | null =
|
||||
walls.length || basin
|
||||
? {
|
||||
minOffset: trimMin,
|
||||
maxOffset: trimMax,
|
||||
...(trimMinElevation != null ? { minElevation: trimMinElevation } : {}),
|
||||
...(trimMaxElevation != null ? { maxElevation: trimMaxElevation } : {}),
|
||||
...(trimMinSlope ? { minSlope: trimMinSlope } : {}),
|
||||
...(trimMaxSlope ? { maxSlope: trimMaxSlope } : {}),
|
||||
}
|
||||
: null;
|
||||
const outletWallFinal = walls.find((wall) => wall.role === "outlet") ?? null;
|
||||
const outletSlope = outletWallFinal ? slopeOf(outletWallFinal) : null;
|
||||
|
||||
@@ -645,16 +681,8 @@ export function computeCulvertLayout(
|
||||
extras: extras.appliedAdjusts,
|
||||
basinExtras: basinExtras.appliedAdjusts,
|
||||
},
|
||||
designTrim:
|
||||
walls.length || basin
|
||||
? {
|
||||
minOffset: trimMin,
|
||||
maxOffset: trimMax,
|
||||
...(trimMinElevation != null ? { minElevation: trimMinElevation } : {}),
|
||||
...(trimMaxElevation != null ? { maxElevation: trimMaxElevation } : {}),
|
||||
...(trimMinSlope ? { minSlope: trimMinSlope } : {}),
|
||||
...(trimMaxSlope ? { maxSlope: trimMaxSlope } : {}),
|
||||
}
|
||||
: null,
|
||||
bodyX:
|
||||
inletBody || outletBody ? { inlet: inletBody?.x ?? 0, outlet: outletBody?.x ?? 0 } : null,
|
||||
designTrim: body && ownTrim ? mergeGuardTrim(ownTrim, body.trim, walls) : ownTrim,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -383,6 +383,8 @@ export function placePipeWall(input: {
|
||||
adjust: WallAdjust;
|
||||
/** d 미지정(null) 시의 기본 자리 — 성토사면 경사길이 4m 지점의 수평 성분. */
|
||||
defaultD: number;
|
||||
/** x 하한 — 기본 0(안쪽 금지) · 세월교 · 박스 흙막이는 본체 끝이 0점이라 안 막음(73-4). */
|
||||
minX?: number;
|
||||
}): PipeWallPlacement {
|
||||
const top0 = input.baseElevation0 + input.height;
|
||||
const dLow = Math.max(
|
||||
@@ -390,7 +392,7 @@ export function placePipeWall(input: {
|
||||
-FILL_SLOPE_RATIO_MIN * (input.invertCap - (top0 - input.height)),
|
||||
);
|
||||
const d = Math.max(input.adjust.d ?? input.defaultD, Math.ceil(dLow * 10) / 10);
|
||||
const x = Math.max(input.adjust.x, 0);
|
||||
const x = Math.max(input.adjust.x, input.minX ?? 0);
|
||||
const topElevation = top0 - d / FILL_SLOPE_RATIO_MIN;
|
||||
return {
|
||||
x,
|
||||
|
||||
@@ -222,6 +222,8 @@ export interface CulvertLayout {
|
||||
* basinLUAllowed = 집수정 ㄴ·ㄷ형(구조물이 원지반으로 밀고 들어갈 때만).
|
||||
*/
|
||||
inletOptions: { revetAllowed: boolean; basinLUAllowed: boolean };
|
||||
/** 세월교 · 박스 흙막이(73-4) — 본체 끝 자리의 배관 4축 x(적용 x 의 0점). 본체 없으면 없음. */
|
||||
bodyX?: { inlet: number; outlet: number } | null;
|
||||
}
|
||||
/** 유입측 구조물 사용자 선택 — auto = 규칙 기반 자동 선택. */
|
||||
export type InletStructureChoice = "auto" | "revet" | "I" | "L" | "U";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 분리(700줄 제한). 카드가 다시 그려질 때마다 한 번 불린다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { bodyAbsolute } from "./B06_Section_UI_Cross_Body_Guard";
|
||||
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import { computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { CulvertLayout, InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
@@ -426,14 +427,15 @@ export function computeCardCulvert(
|
||||
// 조작값만 넘기면 손대기 전(전부 자동)에는 이 측점이 자기 지형으로 자동 자리를 다시
|
||||
// 잡아 소유 측점과 어긋난다 — "한 번 움직이면 맞는다"던 증상의 원인(2026-08-24 사용자).
|
||||
const ownerInput = adjustsInput(link.source, revetOffset, extraWalls);
|
||||
const ownerApplied =
|
||||
computeCulvertLayout(
|
||||
link.source,
|
||||
link.source.samples,
|
||||
ownerInput,
|
||||
link.source.culvert?.hidden_pipe ? "revet" : inletStructure?.valueFor(link.source),
|
||||
inletStructure?.adjustFor(link.source),
|
||||
)?.revetShift ?? ownerInput;
|
||||
// 세월교 · 박스 흙막이는 본체 기준 x 를 절대값으로 바꿔 넘김 — 옆 측점엔 본체가 없음(73-4).
|
||||
const ownerLayout = computeCulvertLayout(
|
||||
link.source,
|
||||
link.source.samples,
|
||||
ownerInput,
|
||||
link.source.culvert?.hidden_pipe ? "revet" : inletStructure?.valueFor(link.source),
|
||||
inletStructure?.adjustFor(link.source),
|
||||
);
|
||||
const ownerApplied = ownerLayout ? bodyAbsolute(ownerLayout) : ownerInput;
|
||||
// 연동을 풀어도 **같은 기슭막이**다 — 단 수는 소유 측점 것이고, 이 측점이 손댄
|
||||
// 벽만 자기 값으로 선다(2026-08-30 사용자).
|
||||
const merged = detached ? detachedAdjusts(section, ownerApplied, revetOffset) : ownerApplied;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 횡단은 종/횡 공통 Y스케일을 쓰지 않고 카드마다 자체 1:1 ppm으로 스케일한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { withoutGuardedFill } from "./B06_Section_UI_Cross_Body_Guard";
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
appendCrossAreaBands,
|
||||
@@ -404,8 +405,15 @@ export function createCrossSectionCard(
|
||||
revetLink,
|
||||
);
|
||||
// 세월교·BOX암거 구체 — 배수관과 그림이 달라 계산·그리기를 따로 탄다(2026-08-25).
|
||||
const boxLayout = computeBoxLayout(section, section.samples, bodies.boxAdjust());
|
||||
const fordLayout = computeFordLayout(section, section.samples, bodies.fordAdjust());
|
||||
// 흙막이(73-4)가 선 쪽 본체 성토선은 걷음 — 그 쪽 성토선은 흙막이 벽이 냄.
|
||||
const boxLayout = withoutGuardedFill(
|
||||
computeBoxLayout(section, section.samples, bodies.boxAdjust()),
|
||||
culvertLayout,
|
||||
);
|
||||
const fordLayout = withoutGuardedFill(
|
||||
computeFordLayout(section, section.samples, bodies.fordAdjust()),
|
||||
culvertLayout,
|
||||
);
|
||||
// 포장층 → 설계선 → 암 경계선 순으로 겹쳐, 설계선이 포장 박스 위에 오게 한다.
|
||||
// 물넘이포장은 노면 자체가 파인 자리라 포장층도 그 바닥을 따른다 — 그렸으면 일반
|
||||
// 포장 박스는 건너뛴다(두 겹으로 깔리면 색 구분이 사라진다).
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 캐시(`section.culvert`·조정창 제어기)만 고친다 — 영구저장은 [저장]·[확정] 몫이다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { pipeDiameterM } from "@util/common_util_culvert_sets";
|
||||
import { guardSet, pipeDiameterM } from "@util/common_util_culvert_sets";
|
||||
import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
revetWallSpec,
|
||||
WALL_MIN_PURE_HEIGHT_M,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { CrossSection, CulvertSet } from "./B06_Section_Api_Fetch";
|
||||
import type { createStationControls } from "./B06_Section_UI_Page_Station_Controls";
|
||||
|
||||
type StationControls = ReturnType<typeof createStationControls>;
|
||||
@@ -69,16 +69,14 @@ export function applyPipeOptionsToCache(
|
||||
if (!owner) return;
|
||||
// 세월교는 스펙 자리(`section.ford`)가 배수관과 달라 조정창 제어기로 보낸다 —
|
||||
// 로직은 그쪽 것을 쓰고 프론트만 폼 UI다(2026-08-30 사용자 확정).
|
||||
if (owner.ford) {
|
||||
applyFordFormOptions(ctx.ford, owner.chainage_m, patch);
|
||||
return;
|
||||
}
|
||||
if (owner.ford) applyFordFormOptions(ctx.ford, owner.chainage_m, patch);
|
||||
// BOX암거도 스펙 자리가 따로다(`section.box`) — 같은 규칙으로 제어기에 보낸다
|
||||
// (2026-08-30 사용자: 세월교와 같은 문제).
|
||||
if (owner.box) {
|
||||
applyBoxFormOptions(ctx.box, owner.chainage_m, patch);
|
||||
return;
|
||||
}
|
||||
else if (owner.box) applyBoxFormOptions(ctx.box, owner.chainage_m, patch);
|
||||
// 세월교 · 박스 흙막이(73-4) — 폼 칸 전부로 세트를 다시 짓고 아래 배관 흙막이 길(높이 · 형태 ·
|
||||
// 추가 흙막이)을 그대로 탐. 일부만 온 되얹기는 세트를 못 지어 건너뜀.
|
||||
const body = owner.ford ? "ford" : owner.box ? "box" : null;
|
||||
if (body && (!whole || !syncBodyGuard(owner, body, patch))) return;
|
||||
const culvert = owner.culvert;
|
||||
if (!culvert) return;
|
||||
const num = (key: string): number | undefined => {
|
||||
@@ -111,7 +109,7 @@ export function applyPipeOptionsToCache(
|
||||
own(side, "revet_length_m", num(`${prefix}_revet_length_m`));
|
||||
own(side, "revet_before_m", num(`${prefix}_revet_before_m`));
|
||||
own(side, "revet_after_m", num(`${prefix}_revet_after_m`));
|
||||
put(side, "structure", patch[`${prefix}_type`]);
|
||||
if (!body) put(side, "structure", patch[`${prefix}_type`]); // 세월교 구조 칸 = 흙막이 고르기
|
||||
}
|
||||
// 배관 벽의 **높이·형태는 스펙이 아니라 조정값**(revet_adjust.h·m)이 정한다
|
||||
// — `revetWallSpec`이 배관에서는 spec.revet_height_m을 쓰지 않기 때문이다.
|
||||
@@ -144,7 +142,14 @@ export function applyPipeOptionsToCache(
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!culvert.hidden_pipe) applySoilTiers(ctx, owner, patch, whole);
|
||||
// 세월교 · 박스 추가 흙막이 = 폼 상자 수가 유출 다단 수(배관은 횡단이 정함 · 73-4)
|
||||
const soil = body ? soilCount(patch) : null;
|
||||
if (soil !== null && soil !== (owner.design?.extra_wall_counts?.outlet ?? 0))
|
||||
// 늦게 불러옴 — 흙막이 세트 때만 쓰는 횡단 기하 묶음(배관 폼 길은 안 끌어옴)
|
||||
void import("../A00_Common/design/design_structure_pipe_calc").then((calc) =>
|
||||
calc.setPipeExtraCount(owner.chainage_m, soil),
|
||||
);
|
||||
if (!culvert.hidden_pipe || body) applySoilTiers(ctx, owner, patch, whole);
|
||||
own(culvert.inlet, "basin_length_m", num("inlet_basin_length_m"));
|
||||
own(culvert.inlet, "basin_before_m", num("inlet_basin_before_m"));
|
||||
own(culvert.inlet, "basin_after_m", num("inlet_basin_after_m"));
|
||||
@@ -165,6 +170,34 @@ export function applyPipeOptionsToCache(
|
||||
}
|
||||
}
|
||||
|
||||
/** 세월교 · 박스 흙막이 세트를 폼 칸 전부로 다시 지음(73-4) — 흙막이가 없으면 걷음. */
|
||||
function syncBodyGuard(
|
||||
owner: CrossSection,
|
||||
body: "box" | "ford",
|
||||
patch: Record<string, number | string>,
|
||||
): boolean {
|
||||
const box = owner.box;
|
||||
const diameter = box
|
||||
? box.top_thickness_m + box.inner_height_m + box.slab_thickness_m
|
||||
: (owner.ford?.diameter_m ?? 1);
|
||||
const guard = guardSet(patch, body, Number(diameter.toFixed(3)));
|
||||
if (!guard) delete owner.culvert;
|
||||
else
|
||||
owner.culvert = {
|
||||
...(guard as unknown as CulvertSet),
|
||||
chainage_m: ((box ?? owner.ford) as { chainage_m?: number } | undefined)?.chainage_m,
|
||||
};
|
||||
return !!guard;
|
||||
}
|
||||
|
||||
/** 추가 흙막이 상자 수 — 가장 큰 `soil_add_{n}_*` 의 n. */
|
||||
function soilCount(patch: Record<string, number | string>): number {
|
||||
return Math.max(
|
||||
0,
|
||||
...Object.keys(patch).map((key) => Number(/^soil_add_(\d+)_/.exec(key)?.[1] ?? 0)),
|
||||
);
|
||||
}
|
||||
|
||||
/** 흙막이 컨테이너가 마지막으로 보낸 높이 · 형태(`측점:extra{n}`) — 기하가 다시 그릴 때마다 키를 지형에
|
||||
* 맞춰 고쳐 되받으므로(syncApplied) 되받은 값과 견주면 안 바꾼 단도 다시 보냄(54-10). */
|
||||
const soilSent = new Map<string, { h: number | null; m: string | null }>();
|
||||
|
||||
@@ -199,6 +199,56 @@ function revetSet(values: Record<string, unknown>): CulvertSetSpec {
|
||||
};
|
||||
}
|
||||
|
||||
/** 세월교 · 박스 유입 · 유출 구조 칸(73-4) — 「흙막이」면 배관 흙막이와 같은 세트를 함께 얹음. */
|
||||
const GUARD_KEYS: Record<"box" | "ford", [string, string]> = {
|
||||
box: ["wing_in", "wing_out"],
|
||||
ford: ["inlet_type", "outlet_type"],
|
||||
};
|
||||
|
||||
/** 세월교 · 박스 흙막이 — 독립 기슭막이와 같은 세트 모양(관 숨김) · 자리 = 관 · 본체 끝(73-4).
|
||||
* 흙막이를 안 고른 쪽 = 구조 빈 값(안 그림) · 둘 다 아니면 null. 파이썬 짝 `_guard_set`. */
|
||||
export function guardSet(
|
||||
values: Record<string, unknown>,
|
||||
body: "box" | "ford",
|
||||
diameterM: number,
|
||||
): CulvertSetSpec | null {
|
||||
const [inletKey, outletKey] = GUARD_KEYS[body];
|
||||
const side = (role: string, key: string): CulvertSetSpec =>
|
||||
String(values[key] ?? "").trim() === "흙막이"
|
||||
? revetSide(values, role)
|
||||
: { role, structure: "" };
|
||||
const inlet = side("inlet", inletKey);
|
||||
const outlet = side("outlet", outletKey);
|
||||
if (!inlet.structure && !outlet.structure) return null;
|
||||
return {
|
||||
type: "pipe",
|
||||
hidden_pipe: true,
|
||||
body,
|
||||
side: "양쪽",
|
||||
tiers: 1,
|
||||
foundation: null,
|
||||
pipe_kind: null,
|
||||
diameter_m: diameterM,
|
||||
min_cover_m: 0.0,
|
||||
inlet,
|
||||
outlet,
|
||||
};
|
||||
}
|
||||
|
||||
/** 박스 본체 전체 높이(상판 + 내공 + 저판) — 흙막이 세트의 관경 자리(파이썬 `wall_body_height`). */
|
||||
function boxBodyHeight(box: CulvertSetSpec): number {
|
||||
return round(
|
||||
Number(box.top_thickness_m) + Number(box.inner_height_m) + Number(box.slab_thickness_m),
|
||||
3,
|
||||
);
|
||||
}
|
||||
|
||||
/** 흙막이가 있으면 세트에 `guard` 로 실음(얹을 때 `culvert` 자리로 감). */
|
||||
function withGuard(spec: CulvertSetSpec, guard: CulvertSetSpec | null): CulvertSetSpec {
|
||||
if (guard) spec.guard = guard;
|
||||
return spec;
|
||||
}
|
||||
|
||||
/** 관경(㎜) → 관 지름(m). 파이썬 `_culvert_set`·`_ford_set` 과 같은 반올림. */
|
||||
export function pipeDiameterM(diameterMm: number | null | undefined): number {
|
||||
return round((num(diameterMm, 1000.0) ?? 1000.0) / 1000.0, 3);
|
||||
@@ -296,7 +346,7 @@ function fordSet(
|
||||
const pipeCount = count ? Math.max(Math.trunc(count), 1) : 1;
|
||||
const wingIn = wingSpec(values, defaults, "in");
|
||||
const wingOut = wingSpec(values, defaults, "out");
|
||||
return {
|
||||
const spec: CulvertSetSpec = {
|
||||
type: "ford",
|
||||
pipe_kind: kind,
|
||||
diameter_m: pipeDiameterM(diameterMm),
|
||||
@@ -321,6 +371,7 @@ function fordSet(
|
||||
...wingDefaulted(values, wingOut, "out"),
|
||||
],
|
||||
};
|
||||
return withGuard(spec, guardSet(values, "ford", Number(spec.diameter_m)));
|
||||
}
|
||||
|
||||
/** 물넘이포장 1개소 — 구조물이 아니라 **파인 노면**이라 형상이 다르다. */
|
||||
@@ -368,7 +419,7 @@ function boxSet(
|
||||
const slab = slabValue && slabValue > 0 ? slabValue : FORD_SLAB_THICKNESS_M;
|
||||
const wingIn = wingSpec(values, defaults, "in");
|
||||
const wingOut = wingSpec(values, defaults, "out");
|
||||
return {
|
||||
const spec: CulvertSetSpec = {
|
||||
type: "box",
|
||||
inner_width_m: innerWidth || 2.0,
|
||||
inner_height_m: innerHeight || 2.0,
|
||||
@@ -388,6 +439,7 @@ function boxSet(
|
||||
...wingDefaulted(values, wingOut, "out"),
|
||||
],
|
||||
};
|
||||
return withGuard(spec, guardSet(values, "box", boxBodyHeight(spec)));
|
||||
}
|
||||
|
||||
/** 관 지점 목록 → 누가거리(소수 2자리)별 세트 제원. */
|
||||
@@ -453,6 +505,9 @@ export function attachCulvertSets(
|
||||
...spec,
|
||||
chainage_m: pipeChainage,
|
||||
};
|
||||
// 세월교 · 박스 흙막이(73-4) — 배관 흙막이와 같은 자리(`culvert`)에 함께 얹음.
|
||||
if (spec.guard)
|
||||
section.culvert = { ...(spec.guard as CulvertSetSpec), chainage_m: pipeChainage };
|
||||
attached += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,119 @@
|
||||
/* 73-4 세월교 · 박스 흙막이 횡단 기하 헬퍼 — 화면 TS(`B06_Section_Structure_Layouts` · `_Cross_Body_Guard`)를
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림. 입력 = 픽스처(검증_sub7 상세 일부) · 결과 = 표준출력 JSON 한 벌.
|
||||
* 짝 = `test_73_4_body_guard.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
// 화면 전용 모듈(토스트 · 글) — 기하 계산엔 안 쓰임
|
||||
const STUBS = {
|
||||
"@ui/ui_template_toast": { showToast: () => undefined },
|
||||
};
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) {
|
||||
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
cache.set(full, data);
|
||||
return data;
|
||||
}
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", f);
|
||||
const { computeStoredLayouts, structureAreaRows, trimOfLayouts } = loadTs(
|
||||
B06("B06_Section_Structure_Layouts.ts"),
|
||||
);
|
||||
const { outletPipeEnd } = loadTs(B06("B06_Section_UI_Cross_Culvert_Tiers.ts"));
|
||||
const { bodyGuardBase } = loadTs(B06("B06_Section_UI_Cross_Body_Guard.ts"));
|
||||
|
||||
const fixture = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
|
||||
);
|
||||
const sections = fixture.cross_sections;
|
||||
const at = (c) => sections.find((s) => Math.abs(s.chainage_m - c) < 0.6);
|
||||
const round = (v) => Math.round(v * 1000) / 1000;
|
||||
|
||||
/** 벽마다 전면(관 끝 자리) ↔ 본체 끝 차이(m) · 바닥 기준 ↔ 본체 끝 표고 차이. */
|
||||
function fronts(section) {
|
||||
const layouts = computeStoredLayouts(section, sections);
|
||||
const base = bodyGuardBase(section, section.samples);
|
||||
const walls = layouts.culvert?.walls ?? [];
|
||||
return {
|
||||
walls: walls.map((w) => {
|
||||
const end = base.ends[w.role];
|
||||
const front = outletPipeEnd(w);
|
||||
return {
|
||||
role: w.role,
|
||||
form: w.form,
|
||||
dOffset: round(front.offset - end.offset),
|
||||
dElevation: round(front.elevation - end.elevation),
|
||||
front: round(front.offset),
|
||||
};
|
||||
}),
|
||||
bodyX: layouts.culvert?.bodyX ?? null,
|
||||
applied: layouts.culvert?.revetShift
|
||||
? { inlet: layouts.culvert.revetShift.inlet.x, outlet: layouts.culvert.revetShift.outlet.x }
|
||||
: null,
|
||||
trimSame:
|
||||
JSON.stringify(trimOfLayouts(layouts)) === JSON.stringify(layouts.culvert?.designTrim),
|
||||
pipeLength: null,
|
||||
};
|
||||
}
|
||||
|
||||
const out = {};
|
||||
const box = at(352.14);
|
||||
const ford = at(173.09);
|
||||
out.box = fronts(box);
|
||||
out.ford = fronts(ford);
|
||||
|
||||
// 박스를 좌우로 1 m 늘리면 벽 전면도 1 m 따라 나감(연동 = 본체 기준 자리)
|
||||
const moved = JSON.parse(JSON.stringify(box));
|
||||
moved.design.box_adjust = { left: { lengthM: 1, riseM: 0 }, right: { lengthM: 1, riseM: 0 } };
|
||||
const swapped = sections.map((s) => (s === box ? moved : s));
|
||||
const movedLayouts = computeStoredLayouts(moved, swapped);
|
||||
const base = bodyGuardBase(moved, moved.samples);
|
||||
out.boxMoved = movedLayouts.culvert.walls.map((w) => ({
|
||||
role: w.role,
|
||||
dOffset: round(outletPipeEnd(w).offset - base.ends[w.role].offset),
|
||||
front: round(outletPipeEnd(w).offset),
|
||||
}));
|
||||
|
||||
// 연동 옆 측점(340 · 360) — 소유 측점 벽이 섬
|
||||
out.linked = [340, 360].map((c) => {
|
||||
const s = at(c);
|
||||
return { chainage: c, walls: computeStoredLayouts(s, sections)?.culvert?.walls.length ?? 0 };
|
||||
});
|
||||
|
||||
// 면적 행 — 서버 Node 진입점과 같은 함수(`structureAreaRows`)
|
||||
out.areas = structureAreaRows(sections).filter((r) =>
|
||||
[173.087, 352.14].some((c) => Math.abs(r.chainage_m - c) < 0.6),
|
||||
);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,82 @@
|
||||
"""73-4 세월교 · 박스 흙막이 — 관보호공 흙막이와 같은 벽 · 같은 자리 규칙(관 · 본체 끝).
|
||||
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_73_4_body_guard.cjs`)과 서버 Node 번들(면적)을 대조함.
|
||||
픽스처 = 검증_sub7 상세 일부(세월교 173.09 유입 · 유출 흙막이 · 박스 352.14 유입 · 유출 흙막이 전후 15 · 10 m).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
HELPER = Path(__file__).parent / "helper_73_4_body_guard.cjs"
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "b06_73_4_body_guard_sections.json"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def screen() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_박스_벽_전면이_본체_끝(screen: dict) -> None:
|
||||
walls = screen["box"]["walls"]
|
||||
assert {w["role"] for w in walls} == {"inlet", "outlet"}
|
||||
for wall in walls:
|
||||
assert wall["dOffset"] == pytest.approx(0, abs=1e-3)
|
||||
assert wall["dElevation"] == pytest.approx(0, abs=1e-3)
|
||||
# 형태 = 폼 값 그대로(유입 찰 · 유출 콘크리트)
|
||||
assert {w["role"]: w["form"] for w in walls} == {"inlet": "돌쌓기(찰)", "outlet": "콘크리트"}
|
||||
# 자동 자리 = 본체 기준 0 — 되받아 저장해도 본체를 따라감
|
||||
assert screen["box"]["applied"] == {"inlet": 0, "outlet": 0}
|
||||
|
||||
|
||||
def test_박스를_늘리면_벽도_m_단위로_따라감(screen: dict) -> None:
|
||||
before = {w["role"]: w["front"] for w in screen["box"]["walls"]}
|
||||
for wall in screen["boxMoved"]:
|
||||
assert wall["dOffset"] == pytest.approx(0, abs=1e-3)
|
||||
assert abs(wall["front"] - before[wall["role"]]) == pytest.approx(1.0, abs=1e-3)
|
||||
|
||||
|
||||
def test_세월교_벽_전면이_관_끝(screen: dict) -> None:
|
||||
walls = {w["role"]: w for w in screen["ford"]["walls"]}
|
||||
assert walls["outlet"]["dOffset"] == pytest.approx(0, abs=1e-3)
|
||||
assert walls["outlet"]["dElevation"] == pytest.approx(0, abs=1e-3)
|
||||
# 유입 2.5 m 벽은 관 끝 자리에선 노면 위로 솟음 — 배관 벽과 같이 상단이 노견에서 멈춰 내려앉음
|
||||
assert walls["inlet"]["dElevation"] <= 0
|
||||
|
||||
|
||||
def test_연동_옆_측점에도_벽이_섬(screen: dict) -> None:
|
||||
assert [row["walls"] for row in screen["linked"]] == [2, 2]
|
||||
|
||||
|
||||
def test_설계선_트림은_흙막이_쪽(screen: dict) -> None:
|
||||
assert screen["box"]["trimSame"] and screen["ford"]["trimSame"]
|
||||
|
||||
|
||||
def test_서버_면적이_화면과_같음(screen: dict) -> None:
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import _NPM_SCRIPT, BUNDLE
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
sections = json.loads(FIXTURE.read_text(encoding="utf-8"))["cross_sections"]
|
||||
output = run_bundle_json(BUNDLE, _NPM_SCRIPT, {"detail": {"cross_sections": sections}})
|
||||
assert output, "서버 Node 번들 실행 실패"
|
||||
server = {round(r["chainage_m"], 2): r for r in output["areas"]}
|
||||
for row in screen["areas"]:
|
||||
mine = server[round(row["chainage_m"], 2)]
|
||||
for key in ("cut_area_m2", "fill_area_m2"):
|
||||
assert mine[key] == pytest.approx(row[key], abs=1e-4), (row["chainage_m"], key)
|
||||
# 세월교 · 박스 흙막이 세트는 관이 아님 — 관 연장 안 실음
|
||||
assert all("pipe_length_m" not in r for r in screen["areas"])
|
||||
@@ -53,6 +53,10 @@ POINTS = [
|
||||
"wing_in_length_m": 3.0,
|
||||
"wing_in_angle_deg": 30.0,
|
||||
"wing_out": "없음",
|
||||
# 세월교 유입 흙막이(73-4)
|
||||
"inlet_type": "흙막이",
|
||||
"inlet_revet_form": "콘크리트",
|
||||
"inlet_revet_height_m": 1.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -64,6 +68,10 @@ POINTS = [
|
||||
"wing_in": "날개벽",
|
||||
"wing_out": "흙막이",
|
||||
"wing_out_length_m": 2.0,
|
||||
# 박스 유출 흙막이(73-4) — 배관 흙막이와 같은 칸 · `culvert` 자리에 함께 얹힘
|
||||
"outlet_revet_form": "돌쌓기(찰)",
|
||||
"outlet_revet_height_m": 3.5,
|
||||
"outlet_revet_length_m": 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -175,3 +183,19 @@ def test_culvert_sets_match_between_python_and_ts(tmp_path: Path) -> None:
|
||||
assert left == right, f"{CHAINAGES[index]}m 측점 세트가 갈렸다"
|
||||
# 시설이 실제로 붙었는지 — 빈 결과끼리 같아서 통과하는 것을 막는다.
|
||||
assert sum(1 for item in expected if len(item) > 1) >= 6
|
||||
|
||||
|
||||
def test_ford_box_guard_attached_as_culvert() -> None:
|
||||
"""세월교 · 박스 흙막이(73-4) — 흙막이 고른 쪽만 기슭막이 · 관 숨김 · 자리 기준 = 본체."""
|
||||
sections = {item["chainage_m"]: item for item in _python_sections()}
|
||||
ford, box = sections[60.0]["culvert"], sections[80.0]["culvert"]
|
||||
assert (ford["body"], ford["hidden_pipe"], ford["diameter_m"]) == ("ford", True, 1.0)
|
||||
assert ford["inlet"]["structure"] == "기슭막이" and ford["outlet"]["structure"] == ""
|
||||
assert ford["inlet"]["revet_form"] == "콘크리트"
|
||||
assert box["body"] == "box" and box["inlet"]["structure"] == ""
|
||||
assert box["outlet"]["revet_height_m"] == 3.5
|
||||
# 본체 전체 높이 = 상판 + 내공 2.5 + 저판
|
||||
b = sections[80.0]["box"]
|
||||
assert box["diameter_m"] == round(b["top_thickness_m"] + 2.5 + b["slab_thickness_m"], 3)
|
||||
# 흙막이 없는 시설엔 안 얹음
|
||||
assert "culvert" not in sections[100.0]
|
||||
|
||||
Reference in New Issue
Block a user