Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -83,11 +83,12 @@ export const revetKeys = (side: "inlet" | "outlet"): Record<string, string> => (
|
||||
});
|
||||
|
||||
/** 세월교 · 박스 유입 · 유출 흙막이 칸(79-7) — 높이 빈 칸 = 자동(`heightHint` · 횡단도가 셈) ·
|
||||
* 절토부 앞 수평공간 칸(`{side}_revet_berm_m`)을 더함. */
|
||||
* 유입만 절토부 앞 수평공간 칸(`inlet_revet_berm_m` · 유출은 없음 · 89-4)을 더함. */
|
||||
export function bodyGuardItems(side: "inlet" | "outlet", heightHint: string): SpecItem[] {
|
||||
const items = guardItems({ 흙막이: revetKeys(side) }, undefined, true).map((item) =>
|
||||
item.id === "height" ? { ...item, default: undefined, hint: heightHint } : item,
|
||||
);
|
||||
if (side === "outlet") return items;
|
||||
const berm: SpecItem = {
|
||||
id: "berm",
|
||||
label: "수평공간 (m)",
|
||||
|
||||
@@ -40,13 +40,16 @@ import {
|
||||
INLET_REVET_KEYS,
|
||||
type InletStructureKind,
|
||||
labeled,
|
||||
numberInput,
|
||||
OUTLET_REVET_KEYS,
|
||||
stepper,
|
||||
WING_IN_KEYS,
|
||||
WING_OUT_KEYS,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { guardChoiceFields, revetChoiceRow } from "./B05_Profile_UI_Drainage_Revet_Choice";
|
||||
import {
|
||||
guardChoiceFields,
|
||||
guardSoilCount,
|
||||
outletTiersRow,
|
||||
revetChoiceRow,
|
||||
} from "./B05_Profile_UI_Drainage_Revet_Choice";
|
||||
import { optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
import {
|
||||
createSpecForm,
|
||||
@@ -67,7 +70,6 @@ import {
|
||||
pipeDrawn,
|
||||
pipeExtraCount,
|
||||
pipeLengthM,
|
||||
setPipeExtraCount,
|
||||
} from "../A00_Common/design/design_structure_pipe_calc";
|
||||
import {
|
||||
hasLinks,
|
||||
@@ -296,25 +298,11 @@ export function createFacilityOptionsForm(
|
||||
return drawnMemo;
|
||||
};
|
||||
// 유출 「추가 흙막이(단)」(54-27) — 값 = 횡단 조정 단 수(같은 저장 자리) · 흙막이 상자 수가 따라감
|
||||
const tiersInput = numberInput("1", "0");
|
||||
tiersInput.max = "9";
|
||||
const tiersRow = labeled("추가 흙막이(단)", stepper(tiersInput, 1, 0));
|
||||
tiersRow.dataset.specItem = "outlet.tiers";
|
||||
tiersRow
|
||||
.querySelectorAll("button")
|
||||
.forEach((b, i) => (b.title = `1단 ${i ? "늘리기" : "줄이기"}`));
|
||||
const paintTiers = (values: Record<string, string>): HTMLElement => {
|
||||
const count = pipeExtraCount(chainage);
|
||||
tiersInput.value = count === null ? "" : String(count);
|
||||
tiersInput.disabled = count === null;
|
||||
tiersRow.hidden = !!values.structure && values.structure !== "기슭막이";
|
||||
return tiersRow;
|
||||
};
|
||||
tiersInput.addEventListener("change", () => {
|
||||
const count = Math.max(0, Math.min(9, Math.round(Number(tiersInput.value) || 0)));
|
||||
// 지형이 못 세운 단은 횡단 기하가 자름 — 끝난 뒤 세운 만큼으로 다시 그림
|
||||
void setPipeExtraCount(chainage, count).then(() => pipeForm.recount());
|
||||
});
|
||||
const tiersRow = outletTiersRow(
|
||||
() => chainage,
|
||||
() => pipeForm.recount(),
|
||||
(structure) => !structure || structure === "기슭막이",
|
||||
);
|
||||
const inletChoice = revetChoiceRow(
|
||||
() => chainage,
|
||||
() => pipeForm.refresh(),
|
||||
@@ -327,7 +315,7 @@ export function createFacilityOptionsForm(
|
||||
// 관 보호공 [연동] · [경사](54-30) — 유입 · 유출 상자 각각 한 줄(같은 값) · 길이 있는 흙막이만(54-40 · 집수정 · 안 고름은 숨김)
|
||||
fields: (c, values) =>
|
||||
c.id === "outlet"
|
||||
? [paintTiers(values), outletChoice.paint(values.structure !== "기슭막이")]
|
||||
? [tiersRow.paint(values), outletChoice.paint(values.structure !== "기슭막이")]
|
||||
: c.id === "inlet"
|
||||
? [inletChoice.paint(values.structure !== "기슭막이")]
|
||||
: c.id === "soil_add" // 추가 흙막이 상자도 같은 줄(54-36 · 상자마다 한 줄 · 같은 값)
|
||||
@@ -407,11 +395,17 @@ export function createFacilityOptionsForm(
|
||||
};
|
||||
};
|
||||
const fordFlowBox = overflow(() => fordForm, "ford");
|
||||
// 유출 흙막이 단 수 = 흙막이 상자 수(관보호공과 같은 방향 · 「+ 흙막이」 없음 · 90-1)
|
||||
const fordForm: SpecForm = createSpecForm(specLinks, {
|
||||
onChange: fordFlowBox.onChange,
|
||||
fields: guardChoiceFields(
|
||||
() => chainage,
|
||||
() => fordForm.refresh(),
|
||||
() => fordForm.recount(),
|
||||
),
|
||||
count: guardSoilCount(
|
||||
() => chainage,
|
||||
() => fordForm.state().outlet?.[0]?.structure,
|
||||
),
|
||||
});
|
||||
|
||||
@@ -473,6 +467,11 @@ export function createFacilityOptionsForm(
|
||||
fields: guardChoiceFields(
|
||||
() => chainage,
|
||||
() => boxForm.refresh(),
|
||||
() => boxForm.recount(),
|
||||
),
|
||||
count: guardSoilCount(
|
||||
() => chainage,
|
||||
() => boxForm.state().outlet?.[0]?.structure,
|
||||
),
|
||||
});
|
||||
// 날개벽 칸 — 세월교 · BOX 가 같은 조각을 쓰던 자리 · 조정 행은 명세 유입 · 유출 칸으로 옮겨 붙음
|
||||
|
||||
@@ -43,7 +43,7 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
|
||||
...revetKeys("inlet"),
|
||||
...revetKeys("outlet"),
|
||||
"inlet_revet_berm_m", // 흙막이 절토부 앞 수평공간(79-7)
|
||||
"outlet_revet_berm_m",
|
||||
"outlet_revet_berm_m", // 칸 없앰(89-4) — 목록에 남겨 다음 [저장] 때 옛 값을 지움
|
||||
],
|
||||
// 물넘이 포장 = 표 03 명세 키(PLAN 52-3 · `design_structure_ford_pavement`) + 월류 폭 · 높이(65-5) — 옛 경사 ·
|
||||
// 두께 · 길이는 폼에서 빠져 이어 붙임 · 도수로/돌붙임 키는 `FORM_OPTION_PREFIXES`
|
||||
@@ -61,7 +61,7 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
|
||||
...revetKeys("inlet"),
|
||||
...revetKeys("outlet"),
|
||||
"inlet_revet_berm_m",
|
||||
"outlet_revet_berm_m",
|
||||
"outlet_revet_berm_m", // 칸 없앰(89-4) — 목록에 남겨 다음 [저장] 때 옛 값을 지움
|
||||
],
|
||||
revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")],
|
||||
};
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { toggleButton } from "./B05_Profile_UI_Panel_Fields";
|
||||
import { labeled, numberInput, stepper } from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import {
|
||||
pipeExtraCount,
|
||||
pipeRevetChoice,
|
||||
setPipeExtraCount,
|
||||
setPipeRevetChoice,
|
||||
} from "../A00_Common/design/design_structure_pipe_calc";
|
||||
|
||||
@@ -54,20 +57,67 @@ export function revetChoiceRow(
|
||||
return { paint };
|
||||
}
|
||||
|
||||
/** 세월교 · 박스 흙막이 상자 줄(65-2 · 배관과 같은 줄) — 흙막이를 고른 유입 · 유출 · 추가 흙막이 상자마다 한 줄.
|
||||
* 명세 폼 `fields` 자리에 그대로 씀. */
|
||||
/** 유출 「추가 흙막이(단)」(54-27 · 90-1 배관 · 세월교 · 박스 공용) — 값 = 횡단 조정 단 수(같은 저장 자리) ·
|
||||
* 흙막이 상자 수가 따라감(폼 `count` 훅 = `pipeExtraCount`). `shown` = 유출 구조 값이 벽인지. */
|
||||
export function outletTiersRow(
|
||||
chainageOf: () => number | null,
|
||||
/** 쓴 뒤 — 지형이 못 세운 단은 횡단 기하가 자름 · 세운 만큼으로 상자를 다시 셈. */
|
||||
recount: () => void,
|
||||
shown: (structure: string | undefined) => boolean,
|
||||
): { paint: (values: Record<string, string>) => HTMLElement } {
|
||||
const input = numberInput("1", "0");
|
||||
input.max = "9";
|
||||
const row = labeled("추가 흙막이(단)", stepper(input, 1, 0));
|
||||
row.dataset.specItem = "outlet.tiers";
|
||||
row.querySelectorAll("button").forEach((b, i) => (b.title = `1단 ${i ? "늘리기" : "줄이기"}`));
|
||||
input.addEventListener("change", () => {
|
||||
const count = Math.max(0, Math.min(9, Math.round(Number(input.value) || 0)));
|
||||
void setPipeExtraCount(chainageOf(), count).then(recount);
|
||||
});
|
||||
return {
|
||||
paint: (values) => {
|
||||
const count = pipeExtraCount(chainageOf());
|
||||
input.value = count === null ? "" : String(count);
|
||||
input.disabled = count === null;
|
||||
row.hidden = !shown(values.structure);
|
||||
return row;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 세월교 · 박스 흙막이 상자 줄(65-2 · 배관과 같은 줄) — 흙막이를 고른 유입 · 유출 · 추가 흙막이 상자마다 한 줄 ·
|
||||
* 유출 상자엔 「추가 흙막이(단)」(90-1). 명세 폼 `fields` 자리에 그대로 씀. */
|
||||
export function guardChoiceFields(
|
||||
chainageOf: () => number | null,
|
||||
written: () => void,
|
||||
recount: () => void,
|
||||
): (c: { id: string }, values: Record<string, string>) => HTMLElement[] {
|
||||
const sides = {
|
||||
inlet: revetChoiceRow(chainageOf, written),
|
||||
outlet: revetChoiceRow(chainageOf, written),
|
||||
};
|
||||
const tiers = outletTiersRow(chainageOf, recount, (structure) => structure === "흙막이");
|
||||
return (c, values) =>
|
||||
c.id === "soil_add"
|
||||
? [revetChoiceRow(chainageOf, written).paint(false)]
|
||||
: c.id === "inlet" || c.id === "outlet"
|
||||
? [sides[c.id].paint(values.structure !== "흙막이")]
|
||||
? [
|
||||
...(c.id === "outlet" ? [tiers.paint(values)] : []),
|
||||
sides[c.id].paint(values.structure !== "흙막이"),
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
/** 세월교 · 박스 흙막이 상자 수(90-1) — 유출이 흙막이면 지형이 세운 유출 단 수 · 아니면 0(옛 상자는
|
||||
* 숨기고 값은 파일에 남김). 명세 폼 `count` 훅 자리. */
|
||||
export function guardSoilCount(
|
||||
chainageOf: () => number | null,
|
||||
outletStructure: () => string | undefined,
|
||||
): (c: { id: string }) => number | null | undefined {
|
||||
return (c) =>
|
||||
c.id !== "soil_add"
|
||||
? undefined
|
||||
: outletStructure() === "흙막이"
|
||||
? pipeExtraCount(chainageOf())
|
||||
: 0;
|
||||
}
|
||||
|
||||
@@ -329,8 +329,8 @@ def _guard_set(values: dict[str, Any], body: str, diameter_m: float) -> dict[str
|
||||
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 guarded:
|
||||
# 절토부 앞 수평공간(79-7) — 흙막이 컨테이너 칸 · 빈 값 = 화면 기본값
|
||||
if guarded and role == "inlet":
|
||||
# 절토부 앞 수평공간(79-7) — 유입만(유출은 없음 · 89-4) · 빈 값 = 화면 기본값
|
||||
sides[role]["revet_berm_m"] = _number(values.get(f"{role}_revet_berm_m"), None)
|
||||
if not any(side["structure"] for side in sides.values()):
|
||||
return None
|
||||
|
||||
@@ -176,10 +176,11 @@ function relocateSideRows(panelRoot: HTMLElement, scroll = true): void {
|
||||
const group = target.closest<HTMLElement>("fieldset");
|
||||
if (!group) return;
|
||||
const ownerSlot = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet");
|
||||
// 배관 유출 다단 = 그 단의 「흙막이」 컨테이너(54-10) — 옛 추가 기슭막이 칸 대신 그 상자를 켬 ·
|
||||
// 단 수 · 옵션은 부모(유출) 칸에 그대로
|
||||
// 배관 · 세월교 · 박스 유출 다단 = 그 단의 「흙막이」 컨테이너(54-10 · 90-1) — 옛 추가 기슭막이 칸
|
||||
// 대신 그 상자를 켬 · 단 수 · 옵션은 부모(유출) 칸에 그대로
|
||||
const soil =
|
||||
ownerSlot === sideSlots.outlet && slotUsable(ownerSlot)
|
||||
(ownerSlot === sideSlots.outlet || ownerSlot === sideSlots.wingOutlet) &&
|
||||
slotUsable(ownerSlot)
|
||||
? ownerSlot
|
||||
.closest(".b05-structure__spec")
|
||||
?.querySelectorAll<HTMLElement>('fieldset[data-spec-container="soil_add"]')[
|
||||
@@ -187,7 +188,7 @@ function relocateSideRows(panelRoot: HTMLElement, scroll = true): void {
|
||||
]
|
||||
: undefined;
|
||||
if (soil) {
|
||||
moveRows(panelRoot, PIPE_OUTLET_ROWS, ownerSlot);
|
||||
moveRows(panelRoot, rowsFor(ownerSlot), ownerSlot);
|
||||
setGroupLabel(soil, "");
|
||||
soil.classList.add("is-active");
|
||||
if (scroll) soil.scrollIntoView({ block: "nearest" });
|
||||
|
||||
@@ -271,3 +271,20 @@ export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
|
||||
import {
|
||||
bodyGuardAxes,
|
||||
bodyGuardBase,
|
||||
guardFloorStart,
|
||||
bodyGuardHeight,
|
||||
fordPipeFit,
|
||||
mergeGuardTrim,
|
||||
@@ -596,7 +597,7 @@ export function computeCulvertLayout(
|
||||
outletBottom: outletWall?.bottomBack.elevation ?? 0,
|
||||
basinStart: culvert.hidden_pipe
|
||||
? inletWall
|
||||
? pipeCorners.inlet.bottom
|
||||
? (guardFloorStart(section, body, inletWall) ?? pipeCorners.inlet.bottom)
|
||||
: null
|
||||
: basinFillStart,
|
||||
basinBottom: culvert.hidden_pipe ? (inletWall?.bottomBack.elevation ?? 0) : basinFillBottom,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 집수정 계류측 두 채널의 입력을 짜고 면적용(미확정 단 제외) 결과를 함께 돌려준다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection, CulvertSideSpec } from "./B06_Section_Api_Fetch";
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
GUARD_CUT_BERM_M,
|
||||
REVET_EMBED_DEPTH_M,
|
||||
@@ -85,17 +85,14 @@ export function buildCulvertTiers(input: {
|
||||
side: culvert?.hidden_pipe ? culvert.inlet.face_side : null,
|
||||
});
|
||||
// 세월교 · 박스 흙막이 끝 절토 = 컨테이너 수평공간 뒤 절토 경사(79-7) · 배관은 종전(3도 선).
|
||||
const cutBerm = (spec: CulvertSideSpec | undefined) =>
|
||||
const cutBerm = (lengthM: number) =>
|
||||
culvert?.body
|
||||
? {
|
||||
lengthM: Math.max(spec?.revet_berm_m ?? GUARD_CUT_BERM_M, 0),
|
||||
slopeRatio: section.design?.cut_slope_ratio ?? 1.0,
|
||||
}
|
||||
? { lengthM: Math.max(lengthM, 0), slopeRatio: section.design?.cut_slope_ratio ?? 1.0 }
|
||||
: undefined;
|
||||
// 성토부선 + 다단 기슭막이(2026-08-22 — 보호공 삭제, 윗면 선만 성토부선으로).
|
||||
// 유출측과 **집수정 계류측**이 같은 체계를 쓴다: 5m 넘으면 다단을 둘 수 있다.
|
||||
const outletInput = {
|
||||
cutBerm: cutBerm(culvert?.outlet),
|
||||
cutBerm: cutBerm(0), // 유출 수평공간 없음 — 끝이 원지반에 묻히면 바로 절토(89-4)
|
||||
startBottomElevation: input.outletBottom,
|
||||
outward: input.outletInfo.outward,
|
||||
groundAt,
|
||||
@@ -109,7 +106,7 @@ export function buildCulvertTiers(input: {
|
||||
// 독립 기슭막이(관 숨김)는 집수정이 없어 이 채널이 **유입측 벽의 성토부선·다단**이다 —
|
||||
// 유출측(`extras`)과 같은 시작점(관 하단 꼭짓점)·같은 `bextra` 키(2026-08-29 사용자).
|
||||
const basinInput = {
|
||||
cutBerm: cutBerm(culvert?.inlet),
|
||||
cutBerm: cutBerm(culvert?.inlet.revet_berm_m ?? GUARD_CUT_BERM_M),
|
||||
startBottomElevation: input.basinBottom,
|
||||
outward: input.inletInfo.outward,
|
||||
groundAt,
|
||||
|
||||
@@ -142,13 +142,6 @@ export function applyPipeOptionsToCache(
|
||||
});
|
||||
}
|
||||
}
|
||||
// 세월교 · 박스 추가 흙막이 = 폼 상자 수가 유출 다단 수(배관은 횡단이 정함 · 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"));
|
||||
@@ -190,14 +183,6 @@ function syncBodyGuard(
|
||||
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 }>();
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
짝: `A00_Common/design/design_structure_guard.ts` · `_road.ts` 선택 컨테이너(머리 · 잇기 · 제목) —
|
||||
시험이 대조.
|
||||
기타(52-14) = 사용자가 적은 상세 종류 이름이 집계 건 이름(`그룹`) · 비면 「기타」.
|
||||
배관 흙막이 = 지형이 세운 유출 다단 수(`흙막이단`)까지만 — 넘친 컨테이너 값은 파일에만(54-11).
|
||||
배관 · 세월교 · 박스 흙막이 = 지형이 세운 유출 다단 수(`흙막이단`)까지만 — 넘친 컨테이너 값은
|
||||
파일에만(54-11 · 90-1) · 세월교 · 박스 유출이 흙막이가 아니면 0.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -30,6 +31,9 @@ SPEC_CONTAINERS: dict[str, tuple[tuple[str, str, str], ...]] = {
|
||||
"etc": (("etc_detail", "getcdetail", "상세 내용"),),
|
||||
}
|
||||
_TEXT = ("structure", "form", "note")
|
||||
#: 흙막이 상자 수 = 지형이 세운 유출 다단 수인 종류(54-11 · 90-1) · 세월교 · 박스 유출 구조 칸
|
||||
_TIERED = ("pipe", "box_culvert", "ford_bridge")
|
||||
_BODY_OUTLET = {"box_culvert": "wing_out", "ford_bridge": "outlet_type"}
|
||||
|
||||
|
||||
def etc_label(type_id: str, options: dict[str, Any] | None) -> dict[str, str]:
|
||||
@@ -70,6 +74,9 @@ def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str
|
||||
"""한 구조물의 선택 컨테이너 인스턴스마다 한 건(n 차례) — 컨테이너 없는 종류는 빈 목록."""
|
||||
rows: list[dict[str, Any]] = []
|
||||
tiers = item.get("흙막이단") # None = 모름(저장된 것 그대로)
|
||||
guard_key = _BODY_OUTLET.get(item["type_id"])
|
||||
if guard_key and str(item["options"].get(guard_key) or "").strip() != "흙막이":
|
||||
tiers = 0 # 유출이 날개벽 · 없음 — 옛 상자는 숨김 · 값은 파일에 남김(90-1)
|
||||
for prefix, link, title in SPEC_CONTAINERS.get(item["type_id"], ()):
|
||||
head = re.compile(rf"^{prefix}_(\d+)_(.+)$")
|
||||
found: dict[int, dict[str, Any]] = {}
|
||||
@@ -80,7 +87,7 @@ def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str
|
||||
for n in sorted(found):
|
||||
if (
|
||||
prefix == "soil_add"
|
||||
and item["type_id"] == "pipe"
|
||||
and item["type_id"] in _TIERED
|
||||
and tiers is not None
|
||||
and n > tiers
|
||||
):
|
||||
|
||||
@@ -213,12 +213,13 @@ export function guardSet(
|
||||
diameterM: number,
|
||||
): CulvertSetSpec | null {
|
||||
const [inletKey, outletKey] = GUARD_KEYS[body];
|
||||
// 절토부 앞 수평공간(79-7) — 흙막이 컨테이너 칸 · 빈 값 = 화면 기본값
|
||||
const side = (role: string, key: string): CulvertSetSpec =>
|
||||
String(values[key] ?? "").trim() === "흙막이"
|
||||
? { ...revetSide(values, role), revet_berm_m: num(values[`${role}_revet_berm_m`], null) }
|
||||
? revetSide(values, role)
|
||||
: { role, structure: "" };
|
||||
const inlet = side("inlet", inletKey);
|
||||
// 절토부 앞 수평공간(79-7) — 유입만(유출은 없음 · 89-4) · 빈 값 = 화면 기본값
|
||||
if (inlet.structure) inlet.revet_berm_m = num(values.inlet_revet_berm_m, null);
|
||||
const outlet = side("outlet", outletKey);
|
||||
if (!inlet.structure && !outlet.structure) return null;
|
||||
return {
|
||||
|
||||
@@ -3407,7 +3407,6 @@
|
||||
"outlet.height": "높이 (m)",
|
||||
"outlet.before": "기준측점 전 (m)",
|
||||
"outlet.after": "기준측점 후 (m)",
|
||||
"outlet.berm": "수평공간 (m)",
|
||||
"soil_add": "흙막이",
|
||||
"soil_add.form": "형태",
|
||||
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
|
||||
@@ -3478,7 +3477,6 @@
|
||||
"outlet.height": "높이 (m)",
|
||||
"outlet.before": "기준측점 전 (m)",
|
||||
"outlet.after": "기준측점 후 (m)",
|
||||
"outlet.berm": "수평공간 (m)",
|
||||
"soil_add": "흙막이",
|
||||
"soil_add.form": "형태",
|
||||
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
|
||||
|
||||
@@ -118,6 +118,8 @@ const PIPE_CALC = {
|
||||
pipeDrawn: () => null,
|
||||
pipeLengthM: () => null,
|
||||
pipeExtraCount: () => Math.min(stored, LIMIT),
|
||||
pipeRevetChoice: () => null,
|
||||
setPipeRevetChoice: () => Promise.resolve(),
|
||||
setPipeExtraCount: (chainage, count) => {
|
||||
writes.push([chainage, count]);
|
||||
stored = count;
|
||||
@@ -127,11 +129,8 @@ const PIPE_CALC = {
|
||||
const STUBS = {
|
||||
"@config/config_frontend": { FORD_MANNING_N: 0.03, FORD_SLOPE: 0.02 },
|
||||
"../A00_Common/design/design_structure_pipe_calc": PIPE_CALC,
|
||||
// 관 보호공 [연동] · [경사] 줄(54-30) — 여기서는 자리만
|
||||
"./B05_Profile_UI_Drainage_Revet_Choice": {
|
||||
revetChoiceRow: () => ({ paint: () => new FakeNode("div") }),
|
||||
guardChoiceFields: () => () => [],
|
||||
},
|
||||
// 관 보호공 [연동] · [경사] 단추(54-30) — 여기서는 자리만 · 단 칸(90-1 공용)은 실제 파일
|
||||
"./B05_Profile_UI_Panel_Fields": { toggleButton: () => new FakeNode("button") },
|
||||
"../../B05_Profile/B05_Profile_UI_Structures_Summary_Row": {
|
||||
buildSummaryRow: () => {
|
||||
const root = new FakeNode("div");
|
||||
@@ -169,7 +168,13 @@ const form = createFacilityOptionsForm();
|
||||
form.setChainage(100);
|
||||
form.setFacility("pipe", { pipe_diameter_mm: 1000 });
|
||||
|
||||
const spec = form.root.querySelector(".b05-structure__spec");
|
||||
// 시설마다 명세 폼이 따로 — 지금 시설의 폼(본체 상자 id 로 찾음)
|
||||
let body = null;
|
||||
const specOf = () =>
|
||||
form.root
|
||||
.querySelectorAll(".b05-structure__spec")
|
||||
.find((s) => !body || s.children.some((k) => k.dataset?.specContainer === body));
|
||||
let spec = specOf();
|
||||
const boxesOf = (id) => spec.children.filter((k) => k.dataset?.specContainer === id);
|
||||
const tierRow = () =>
|
||||
boxesOf("outlet")[0]
|
||||
@@ -199,10 +204,41 @@ async function main() {
|
||||
stored = 2;
|
||||
form.recount();
|
||||
out.fromCross = snap();
|
||||
out.writes = writes;
|
||||
// 다른 구조(옛 값)면 칸을 가림
|
||||
form.setFacility("pipe", { pipe_diameter_mm: 1000, outlet_type: "옛값" });
|
||||
out.otherHidden = tierRow()?.hidden ?? null;
|
||||
// 세월교 · 박스 유출 흙막이(90-1) — 같은 단 칸 · 단 수 = 흙막이 상자 수 · 「+ 흙막이」 없음 ·
|
||||
// 유출이 흙막이가 아니면 단 칸 · 옛 상자 숨김(값은 남김)
|
||||
const addButton = () =>
|
||||
spec.children.find((k) => k.dataset?.specAdd === "soil_add") ? true : false;
|
||||
const old = { soil_add_1_form: "바자", soil_add_2_form: "돌망태", soil_add_3_form: "바자" };
|
||||
for (const [facility, key, container] of [
|
||||
["ford_bridge", "outlet_type", "ford"],
|
||||
["box_culvert", "wing_out", "body"],
|
||||
]) {
|
||||
stored = 0;
|
||||
body = container;
|
||||
form.setFacility(facility, { [key]: "흙막이", ...old });
|
||||
spec = specOf();
|
||||
const steps = {};
|
||||
for (const n of ["2", "0"]) {
|
||||
const input = tierInput();
|
||||
input.value = n;
|
||||
input.dispatchEvent(new Event("change"));
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
steps[`set${n}`] = { ...snap(), hidden: tierRow()?.hidden ?? null, add: addButton() };
|
||||
}
|
||||
form.setFacility(facility, { [key]: "날개벽", ...old });
|
||||
steps.wing = {
|
||||
boxes: boxesOf("soil_add").length,
|
||||
hidden: tierRow()?.hidden ?? null,
|
||||
add: addButton(),
|
||||
kept: Object.keys(form.readOptions()).filter((k) => k.startsWith("soil_add_")).length,
|
||||
};
|
||||
out[facility] = steps;
|
||||
}
|
||||
out.writes = writes;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
}
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -137,9 +137,14 @@ out.boxWing = wingLayout.sides.map((s) => ({
|
||||
// ⑤ 흙막이 끝 절토 = 수평공간 뒤 절토 경사 — 수평공간 칸 값을 따름
|
||||
function cutTails(section, list) {
|
||||
const culvert = computeStoredLayouts(section, list).culvert;
|
||||
return [...culvert.outletFill.segments, ...culvert.basinFill.segments]
|
||||
const tagged = (segments, role) => segments.map((g) => ({ ...g, role }));
|
||||
return [
|
||||
...tagged(culvert.outletFill.segments, "outlet"),
|
||||
...tagged(culvert.basinFill.segments, "inlet"),
|
||||
]
|
||||
.filter((g) => g.kind === "cut")
|
||||
.map((g) => ({
|
||||
role: g.role,
|
||||
points: g.points.length,
|
||||
berm: r3(Math.abs(g.points[1].offset - g.points[0].offset)),
|
||||
flat: r3(g.points[1].elevation - g.points[0].elevation),
|
||||
@@ -148,12 +153,25 @@ function cutTails(section, list) {
|
||||
out.cutDefault = [cutTails(at(352.14), sections), cutTails(at(173.087), sections)];
|
||||
const berm2 = clone(at(352.14));
|
||||
berm2.culvert.inlet.revet_berm_m = 2;
|
||||
berm2.culvert.outlet.revet_berm_m = 2;
|
||||
berm2.culvert.outlet.revet_berm_m = 2; // 유출은 칸이 없어 무시(89-4)
|
||||
out.cutBerm2 = cutTails(
|
||||
berm2,
|
||||
sections.map((s) => (s === at(352.14) ? berm2 : s)),
|
||||
);
|
||||
|
||||
// ⑤b 유입 수평공간 시작 높이 = 본체 바닥(세월교 바닥판 상면 · 박스 저판 상면 · 89-3)
|
||||
out.inletFloor = [352.14, 173.087].map((chainage) => {
|
||||
const section = at(chainage);
|
||||
const layouts = computeStoredLayouts(section, sections);
|
||||
const start = layouts.culvert.basinFill.segments[0]?.points[0];
|
||||
const inletLeft = (section.uphill_side ?? "left") === "left";
|
||||
const floor = layouts.ford
|
||||
? layouts.ford.sides.find((s) => s.role === "inlet").slabTopElevation
|
||||
: layouts.box.sides.find((s) => (s.role === "left") === inletLeft).bottomElevation +
|
||||
layouts.box.box.slab_thickness_m;
|
||||
return { start: start ? r3(start.elevation) : null, floor: r3(floor) };
|
||||
});
|
||||
|
||||
// ⑥ 관 보호공 기본 자리 = 제일 위(성토선 0 지점 · d 0) · 관 길이 m 단위
|
||||
const pipe = clone(at(180));
|
||||
pipe.culvert = {
|
||||
|
||||
@@ -89,6 +89,8 @@ def test_다단_벽_고르기는_흙막이_컨테이너로() -> None:
|
||||
dock = (ROOT / "B06_Section" / "B06_Section_UI_Adjust_Dock.ts").read_text(encoding="utf-8")
|
||||
assert 'fieldset[data-spec-container="soil_add"]' in dock
|
||||
assert "panelRoot.dataset.extraIndex" in dock
|
||||
# 세월교 · 박스 유출 다단도 그 상자로(90-1 · 유출 자리 = 날개벽 칸)
|
||||
assert "ownerSlot === sideSlots.outlet || ownerSlot === sideSlots.wingOutlet" in dock
|
||||
panel = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Structure_Panel.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
@@ -45,7 +45,19 @@ def test_지형_한도를_넘으면_세운_만큼(out) -> None:
|
||||
|
||||
|
||||
def test_좌측_칸은_횡단_단_수_자리에_씀(out) -> None:
|
||||
assert out["writes"] == [[100, 2], [100, 1], [100, 5], [100, 0]]
|
||||
assert out["writes"][:4] == [[100, 2], [100, 1], [100, 5], [100, 0]]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("facility", ["ford_bridge", "box_culvert"])
|
||||
def test_세월교_박스_유출도_같은_단_칸(out, facility) -> None:
|
||||
"""90-1 — 단 수 = 흙막이 상자 수 · 「+ 흙막이」 없음 · 유출이 흙막이 아니면 칸 · 옛 상자 숨김(값 남김)."""
|
||||
steps = out[facility]
|
||||
assert (steps["set2"]["value"], steps["set2"]["boxes"]) == ("2", 2)
|
||||
assert (steps["set0"]["value"], steps["set0"]["boxes"]) == ("0", 0)
|
||||
assert steps["set2"]["structures"] == ["형태", "형태"]
|
||||
assert not steps["set2"]["hidden"] and not steps["set2"]["add"]
|
||||
assert steps["wing"] == {"boxes": 0, "hidden": True, "add": False, "kept": 3}
|
||||
assert out["writes"][4:] == [[100, 2], [100, 0]] * 2
|
||||
|
||||
|
||||
def test_횡단에서_바꾸면_좌측_칸이_따라감(out) -> None:
|
||||
|
||||
@@ -35,7 +35,7 @@ GUARD = [
|
||||
["height", ["흙막이"], None], # 빈 칸 = 본체 끝 높이(79-7 · 횡단도가 셈)
|
||||
["before", ["흙막이"], "5"],
|
||||
["after", ["흙막이"], "5"],
|
||||
["berm", ["흙막이"], "1"], # 절토부 앞 수평공간(79-7)
|
||||
["berm", ["흙막이"], "1"], # 절토부 앞 수평공간(79-7) — 유입만(89-4)
|
||||
]
|
||||
|
||||
|
||||
@@ -55,7 +55,8 @@ def out() -> dict:
|
||||
def test_유입_유출은_구조와_흙막이_칸만(out):
|
||||
for spec in ("ford", "box"):
|
||||
for side in ("inlet", "outlet"):
|
||||
assert out[spec][side] == GUARD, (spec, side)
|
||||
want = GUARD if side == "inlet" else GUARD[:-1]
|
||||
assert out[spec][side] == want, (spec, side)
|
||||
assert [i[0] for i in out[spec]["soil_add"]] == [
|
||||
"form",
|
||||
"length",
|
||||
@@ -133,18 +134,28 @@ def test_자동_배치_새_세월교는_기본값과_월류_높이():
|
||||
assert "ford_height_m" not in blind.options # 유량 없으면 빈칸
|
||||
|
||||
|
||||
def test_집계표_세월교_흙막이_상자_건():
|
||||
item = {
|
||||
"type_id": "ford_bridge",
|
||||
"options": {"soil_add_1_form": "바자", "soil_add_2_form": "돌망태"},
|
||||
"복사본": "pipe@50.0",
|
||||
"흙막이단": 0, # 배관 단 수는 세월교에 안 걸림
|
||||
}
|
||||
rows = container_rows(item, {"gsoiladd2": "SS000001"})
|
||||
assert [(r["그룹"], r["summary_item"]) for r in rows] == [
|
||||
("흙막이 1", None),
|
||||
("흙막이 2", "SS000001"),
|
||||
]
|
||||
def test_집계표_세월교_박스_흙막이_상자는_유출_단_수만큼():
|
||||
"""90-1 — 관보호공과 같은 방향(단 수 → 상자) · 유출이 흙막이가 아니면 옛 상자는 집계에서 뺌."""
|
||||
soil = {"soil_add_1_form": "바자", "soil_add_2_form": "돌망태"}
|
||||
|
||||
def groups(type_id: str, outlet: str, tiers: int | None) -> list[tuple[str, object]]:
|
||||
key = {"ford_bridge": "outlet_type", "box_culvert": "wing_out"}[type_id]
|
||||
item = {
|
||||
"type_id": type_id,
|
||||
"options": {**soil, key: outlet},
|
||||
"복사본": "pipe@50.0",
|
||||
"흙막이단": tiers,
|
||||
}
|
||||
rows = container_rows(item, {"gsoiladd2": "SS000001"})
|
||||
return [(r["그룹"], r["summary_item"]) for r in rows]
|
||||
|
||||
for type_id in ("ford_bridge", "box_culvert"):
|
||||
both = [("흙막이 1", None), ("흙막이 2", "SS000001")]
|
||||
assert groups(type_id, "흙막이", 2) == both
|
||||
assert groups(type_id, "흙막이", 1) == both[:1]
|
||||
assert groups(type_id, "흙막이", None) == both # 모름 = 저장된 것 그대로
|
||||
assert groups(type_id, "날개벽", 2) == []
|
||||
assert groups(type_id, "", None) == []
|
||||
|
||||
|
||||
def test_B06_세월교_박스_칸은_측벽_높이_줄만_받음():
|
||||
|
||||
@@ -79,12 +79,20 @@ def test_박스_날개벽_끝_절성토선(shape: dict) -> None:
|
||||
def test_흙막이_끝_절토는_수평공간_뒤(shape: dict) -> None:
|
||||
tails = [tail for group in shape["cutDefault"] for tail in group]
|
||||
assert tails, "절토 끝선이 없음"
|
||||
assert any(tail["role"] == "inlet" for tail in tails)
|
||||
for tail in tails:
|
||||
assert tail["points"] == 3
|
||||
assert tail["flat"] == pytest.approx(0, abs=1e-6)
|
||||
assert tail["berm"] == pytest.approx(1.0, abs=1e-3)
|
||||
# 유출은 수평공간 없음 — 묻히면 바로 절토(89-4)
|
||||
assert tail["berm"] == pytest.approx(1.0 if tail["role"] == "inlet" else 0, abs=1e-3)
|
||||
for tail in shape["cutBerm2"]:
|
||||
assert tail["berm"] == pytest.approx(2.0, abs=1e-3)
|
||||
assert tail["berm"] == pytest.approx(2.0 if tail["role"] == "inlet" else 0, abs=1e-3)
|
||||
|
||||
|
||||
def test_유입_수평공간_시작은_본체_바닥(shape: dict) -> None:
|
||||
"""89-3 — 박스 저판 상면 · 세월교 바닥판 상면 높이에서 수평 → 절토."""
|
||||
for case in shape["inletFloor"]:
|
||||
assert case["start"] == pytest.approx(case["floor"], abs=1e-3)
|
||||
|
||||
|
||||
def test_관_보호공_기본_자리는_제일_위(shape: dict) -> None:
|
||||
|
||||
Reference in New Issue
Block a user