feat(B06): 세월교 · 박스 흙막이 횡단 그림 — 관보호공 흙막이와 같은 벽 · 자리 · 연동 · 경사(73-4)
- 유입 · 유출 구조 「흙막이」면 배관 흙막이와 같은 세트(관 숨김 · 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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -220,6 +220,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 }>();
|
||||
|
||||
Reference in New Issue
Block a user