Merge remote-tracking branch 'origin/dev' into sub_laptop_8
This commit is contained in:
@@ -19,7 +19,7 @@ B05 · B06 한 페이지 뼈대 — 주소 둘(`#/b05-profile` · `#/b06-section
|
||||
|
||||
서버 Node 묶음:
|
||||
|
||||
- 입구 `logic/design_calc_node.ts`(`build:design-calc` · 계산 등록부 `logic/design_calc_registry.ts` 의 `node:` 줄 · 시험 `test_472_3_calc_registry.py`) · 끌려감 = `design_pick_match` · `design_pipe_ids` · `design_selection`(corridor · server-calc · b07-extent) · `design_store` · `design_structure_{box,ford,ford_pavement,guard,pipe,road,spec}`(b07-extent · b07-note) · 상위 `A00_Common/b_page_state`
|
||||
- 입구 `logic/design_calc_node.ts`(`build:design-calc` · 계산 등록부 `logic/design_calc_registry.ts` 의 `node:` 줄 · 시험 `test_472_3_calc_registry.py`) · 끌려감 = `design_pick_match` · `design_pipe_ids` · `design_selection`(b07-extent) · `design_store` · `design_structure_{box,ford,ford_pavement,guard,pipe,road,spec}`(b07-extent · b07-note) · 상위 `A00_Common/b_page_state`
|
||||
- 끌려가는 파일 모듈 맨 위에 DOM · window · sessionStorage 금지 · 고치면 해당 build 다시 지어 시험
|
||||
|
||||
남의 페이지가 부르는 파일(고치면 부르는 쪽 시험도):
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* ① 고름 · 그림 둘 다 구조물 id 가 있으면 id 가 같아야(관 = 세션 id `design_pipe_ids` · 옮겨도 그대로)
|
||||
* ② 아니면 같은 자리(`PIPE_MATCH_M` — 같은 자리 거리는 이 한 값)
|
||||
* ③ 고른 부분(부재키)이 있으면 그 부분만 · 부분을 안 골랐으면 그 구조물 그림 모두
|
||||
* DOM 은 `data-structure-id` · `data-part`(`tagShape`) — 부분 그림(관 · 벽 · 날개벽 · 물넘이 콘크리트)은
|
||||
* DOM 은 `data-structure-id` · `data-part`(`common_util_pick_tag.tagShape`) — 부분 그림(관 · 벽 · 날개벽 · 물넘이 콘크리트)은
|
||||
* 조정창 손잡이가 같은 고름으로 칠함(`paintPicked` 는 부분 없는 형상만 · 307 「측점 길은 부분 안 켬」).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -32,12 +32,6 @@ export function pickLights(selection: PickSelection, tag: PickTag): boolean {
|
||||
return !selection.partKey || tag.part === selection.partKey;
|
||||
}
|
||||
|
||||
/** 그린 형상에 신원표를 닮(부분 없으면 통째 형상). */
|
||||
export function tagShape(el: Element, structureId: string | null, part?: string | null): void {
|
||||
if (structureId) el.setAttribute("data-structure-id", structureId);
|
||||
if (part) el.setAttribute("data-part", part);
|
||||
}
|
||||
|
||||
/** 카드 안 통째 형상 칠하기 — 고른 구조물 id 와 같은 형상만. 부분 그림(`data-part`) · 관 계열(`pipe-`) ·
|
||||
* 노면공 포장층은 제 손잡이(조정창 · 포장층 고름)가 칠함 — 측점 길(알약 · 목록 · 측점선)은 부분을 안 켬(307). */
|
||||
export function paintPicked(root: ParentNode, selection: PickSelection): void {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* 프로젝트가 없으면(시험 · 도움 파일) 옛 자리 이름.
|
||||
* ========================================================================== */
|
||||
|
||||
import { setPipeTagSource } from "../../common_util/common_util_pick_tag";
|
||||
import { readState, writeState } from "../b_page_state";
|
||||
import { isNear, nearestWithin, readSelection, selectDesign } from "./design_selection";
|
||||
|
||||
@@ -26,6 +27,8 @@ let active: string | null = null;
|
||||
/** 지금 프로젝트 — 종단 · 횡단 보기가 열 때. */
|
||||
export function usePipeIds(projectId: string | null): void {
|
||||
active = projectId;
|
||||
// 그림 신원표(common_util_pick_tag)가 이 장부를 씀 — 프로젝트 없으면 옛 자리 이름(pipeIdAt 와 같음)
|
||||
setPipeTagSource(projectId ? (chainageM) => pipeIdAt(chainageM) : null);
|
||||
}
|
||||
|
||||
const bookOf = (projectId: string): Book =>
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
* context?: { earthwork_conversion · natural_spoil_min_ground_slope ·
|
||||
* haul_equipment_limits · 채집석 · 구조물 잔토(B08) } ·
|
||||
* excavation?: { pipes: [{ diameter_mm }] · walls: [{ height_m · average_thickness_m ·
|
||||
* has_foundation }] } } }
|
||||
* has_foundation }] } ·
|
||||
* cross_design?: { standard: 표준 횡단(config 위에 저장분 덮은 한 벌) ·
|
||||
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } } }
|
||||
* 출력 { results: { [id]: 값 }, available: [id…] }
|
||||
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
|
||||
* server-calc 묶음과 같은 차례).
|
||||
@@ -17,6 +19,9 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import type { BermSpec } from "@util/common_util_cross_berm";
|
||||
import { computeCrossDesign } from "@util/common_util_cross_design";
|
||||
import type { CrossDesignOptions, CrossGroundSample } from "@util/common_util_cross_design";
|
||||
import { pipeTrenchWidthM, wallTrenchAreaM2, wallTrenchWidthM } from "@util/common_util_excavation";
|
||||
import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul";
|
||||
import type { MassHaulResult } from "@util/common_util_mass_haul";
|
||||
@@ -58,6 +63,63 @@ interface CalcInputs {
|
||||
has_foundation?: boolean;
|
||||
}>;
|
||||
};
|
||||
cross_design?: {
|
||||
standard: CrossDesignOptions["standard"];
|
||||
stations?: Array<{
|
||||
samples: CrossGroundSample[];
|
||||
design_elevation_m: number | null;
|
||||
options: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
/** 파이썬 `compute_cross_design` 키 → TS 짝 키(거울 시험 test_b06_cross_design_mirror 표와 같음). */
|
||||
const CROSS_OPTION_KEYS: Record<string, keyof CrossDesignOptions> = {
|
||||
ground_type: "groundType",
|
||||
section_mode: "sectionMode",
|
||||
ditch_side: "ditchSide",
|
||||
ditch_type: "ditchType",
|
||||
paved: "paved",
|
||||
rock_boundary_offset_m: "rockBoundaryOffsetM",
|
||||
two_stage_slope: "twoStageSlope",
|
||||
cut_slope_ratio: "cutSlopeRatio",
|
||||
ditch_enabled: "ditchEnabled",
|
||||
ditch_choice: "ditchChoice",
|
||||
surface_drop_m: "surfaceDropM",
|
||||
plan_radius_m: "planRadiusM",
|
||||
curve_outer_side: "curveOuterSide",
|
||||
curve_widening_m: "curveWideningM",
|
||||
shoulder_extend_left_m: "shoulderExtendLeftM",
|
||||
shoulder_extend_right_m: "shoulderExtendRightM",
|
||||
tilted: "tilted",
|
||||
tilt_pct: "tiltPct",
|
||||
};
|
||||
|
||||
/** 파이썬 BermSpec(NamedTuple → dict) → TS BermSpec. */
|
||||
function bermOf(value: unknown): BermSpec | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const berm = value as Record<string, number | null>;
|
||||
return {
|
||||
widthM: berm.width_m as number,
|
||||
intervalM: berm.interval_m as number,
|
||||
slopeDeg: berm.slope_deg as number,
|
||||
depthM: berm.depth_m as number,
|
||||
depthLeftM: berm.depth_left_m ?? null,
|
||||
depthRightM: berm.depth_right_m ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function crossOptions(
|
||||
raw: Record<string, unknown>,
|
||||
standard: CrossDesignOptions["standard"],
|
||||
): CrossDesignOptions {
|
||||
const options: Record<string, unknown> = { standard };
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
if (key === "berm") options.berm = bermOf(value);
|
||||
else if (key in CROSS_OPTION_KEYS) options[CROSS_OPTION_KEYS[key]] = value;
|
||||
else throw new Error(`모르는 횡단 설계 키: ${key}`);
|
||||
}
|
||||
return options as unknown as CrossDesignOptions;
|
||||
}
|
||||
|
||||
/** 한 번 돌림 — 같은 id 는 한 번만 셈(memo) · 앞 id 는 `need` 로. */
|
||||
@@ -117,6 +179,18 @@ function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
),
|
||||
})),
|
||||
}),
|
||||
// 측점마다 TS 짝(`computeCrossDesign`) — 못 세우면 그 자리 { error }(파이썬은 ValueError 로 되던짐).
|
||||
"cross.design": () => {
|
||||
const batch = inputs.cross_design;
|
||||
return (batch?.stations ?? []).map((station) => {
|
||||
try {
|
||||
const options = crossOptions(station.options, batch!.standard);
|
||||
return computeCrossDesign(station.samples, station.design_elevation_m, options);
|
||||
} catch (error) {
|
||||
return { error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
});
|
||||
},
|
||||
// 편집 전 정본 — 계획선 샘플은 저장분 그대로(corridor 묶음과 같은 값) · 측점 둘 미만이면 null.
|
||||
"corridor.mesh": () => {
|
||||
const detail = inputs.detail;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
서버 Node 묶음:
|
||||
|
||||
- 입구 `B05_Profile_Corridor_Node.ts`(package.json `build:corridor`) · 끌어감 = `UI_Corridor_*`(Build · Envelope · Frames · Station · Structures) · `UI_IrregularStations` · B06 `UI_Cross_*` 구조물 · A00 `b_page_state` · `design_pick_match` · `design_pipe_ids` · `design_selection`
|
||||
- 입구 `B05_Profile_Corridor_Node.ts`(package.json `build:corridor`) · 끌어감 = `UI_Corridor_*`(Build · Envelope · Frames · Station · Structures) · B06 `UI_Cross_*` 구조물(화면 · 세션 안 끔 · `test_472_3_calc_registry.py`)
|
||||
- `build:b07-extent` · `build:b07-note` 가 `Api_Structures` · `Api_Pipes_Draft` · `UI_Structures_Balloon` · `UI_Structures_Define` 를 끌어감
|
||||
- 묶음이 끄는 TS 를 고치면 그 build 다시 지어 시험 · 그 파일 모듈 맨 위에 DOM · window · sessionStorage 금지
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
"""B06 라우터의 횡단 설계 파일 읽기와 프리뷰 계산.
|
||||
|
||||
짝(349-0n): `pavement_ranges` · `ford_surface_drops` · `paved_at` · `ford_drop_at` ↔
|
||||
`B06_Section/logic/B06_Section_Cross_Enforce.ts` · 거울 시험 `test_349_0n_cross_enforce_mirror.py`.
|
||||
포장 · 세월교 구간 읽기 · 판정(짝 349-0n) = `B06_Section_Router_Design_Ranges.py` · 다시 내보냄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections import find_cross_file
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets
|
||||
from B06_Section.B06_Section_Engine_Design import (
|
||||
ROUTE_CARRY_KEYS,
|
||||
compute_cross_design,
|
||||
@@ -21,6 +16,16 @@ from B06_Section.B06_Section_Engine_Design import (
|
||||
stored_tilt,
|
||||
)
|
||||
from B06_Section.B06_Section_Engine_Tilt import tilt_args, tilt_ranges
|
||||
|
||||
# 포장 · 세월교 구간(나눈 파일) — 이 모듈 이름으로 다시 내보냄(부르는 곳 · 시험 갈아 끼움 그대로).
|
||||
from B06_Section.B06_Section_Router_Design_Ranges import ( # noqa: F401
|
||||
ford_drop_at,
|
||||
ford_pavement_ranges,
|
||||
ford_surface_drops,
|
||||
paved_at,
|
||||
pavement_ranges,
|
||||
pavement_suggestions,
|
||||
)
|
||||
from common_util.common_util_cross_berm import (
|
||||
BERM_DEFAULT_DEPTH_M,
|
||||
BERM_DEFAULT_INTERVAL_M,
|
||||
@@ -29,15 +34,8 @@ from common_util.common_util_cross_berm import (
|
||||
BermSpec,
|
||||
)
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 세월교 노면 하강이 걸리는 측점 판정 허용 오차(m) — 세트 부착(`attach_culvert_sets`)과
|
||||
# 같은 기준이라 "구체가 그려진 측점"과 "노면이 내려간 측점"이 어긋나지 않는다.
|
||||
_FORD_DROP_TOLERANCE_M = 0.02
|
||||
|
||||
PREVIEW_DESIGN_FIELDS = (
|
||||
"ground_type",
|
||||
"roadbed_width_m",
|
||||
@@ -52,98 +50,6 @@ PREVIEW_DESIGN_FIELDS = (
|
||||
)
|
||||
|
||||
|
||||
def pavement_suggestions(longitudinal: dict[str, Any]) -> dict[float, bool]:
|
||||
mapping: dict[float, bool] = {}
|
||||
stations = longitudinal.get("stations")
|
||||
for station in stations if isinstance(stations, list) else []:
|
||||
suggested = station.get("pavement_suggested")
|
||||
if isinstance(suggested, bool):
|
||||
mapping[round(float(station.get("chainage_m", 0.0)), 3)] = suggested
|
||||
return mapping
|
||||
|
||||
|
||||
def pavement_ranges(project_root: Path) -> list[tuple[float, float]]:
|
||||
"""포장으로 볼 누가거리 구간 — 구조물 정본 G군 + 물넘이포장(포장 필수).
|
||||
|
||||
포장 지정은 **사용자가 구간으로 준다**(2026-08-28 사용자 확정). 종단경사 자동 판정은
|
||||
더 이상 포장을 켜지 않는다 — 경고 표기(`pavement_suggested`)만 남는다.
|
||||
물넘이포장은 콘크리트 등으로 노면을 만드는 시설이라 범위 안이 항상 포장이다.
|
||||
읽기 실패는 비치명 — 빈 목록이면 사용자 지정이 없는 것과 같다.
|
||||
"""
|
||||
ranges: list[tuple[float, float]] = []
|
||||
try:
|
||||
types = structure_type_map()
|
||||
for structure in load_structures(str(project_root))[1]:
|
||||
definition = types.get(structure.type_id)
|
||||
if definition is None or definition.group != "G":
|
||||
continue
|
||||
anchor = structure.chainage_m
|
||||
start = structure.start_m if structure.start_m is not None else anchor
|
||||
end = structure.end_m if structure.end_m is not None else anchor
|
||||
if start is None or end is None:
|
||||
continue
|
||||
ranges.append((min(float(start), float(end)), max(float(start), float(end))))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 포장 구간을 읽지 못했습니다 (사용자 지정 없음으로 본다)")
|
||||
return ranges + ford_pavement_ranges(project_root)
|
||||
|
||||
|
||||
def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""물넘이포장 범위(기준 ± 월류 폭/2) — 포장 필수 · 측구 없음(286-2). 판정은 `paved_at`."""
|
||||
if project_root is None:
|
||||
return []
|
||||
sets = load_culvert_sets(project_root).items()
|
||||
halves = [
|
||||
(at, float(s.get("span_m") or 0.0) / 2.0)
|
||||
for at, s in sets
|
||||
if s.get("type") == "ford_pavement"
|
||||
]
|
||||
return [(at - half, at + half) for at, half in halves]
|
||||
|
||||
|
||||
def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)].
|
||||
|
||||
구체 위 노면은 월류 높이만큼 낮게 앉는다(2026-08-30 사용자 확정). 계획고를 그만큼
|
||||
내려 잡아야 측벽·바닥판이 맞고 절·성토 면적도 따라온다. 포장 구간과 같은 방식으로
|
||||
관 지점 정본에서 읽는다 — 읽기 실패는 비치명(하강 없음으로 본다).
|
||||
"""
|
||||
if project_root is None:
|
||||
return []
|
||||
drops: list[tuple[float, float]] = []
|
||||
try:
|
||||
for chainage, spec in load_culvert_sets(project_root).items():
|
||||
if spec.get("type") != "ford":
|
||||
continue
|
||||
depth = float(spec.get("overflow_depth_m") or 0.0)
|
||||
if depth > 0:
|
||||
drops.append((chainage, depth))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 세월교 월류 높이를 읽지 못했습니다 (노면 하강 없음으로 본다)")
|
||||
return drops
|
||||
|
||||
|
||||
def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float:
|
||||
"""그 측점의 노면 하강량(m). 구체가 앉은 측점만 — 붙는 기준은 세트 부착과 같다."""
|
||||
for at, depth in drops:
|
||||
if abs(chainage_m - at) <= _FORD_DROP_TOLERANCE_M:
|
||||
return depth
|
||||
return 0.0
|
||||
|
||||
|
||||
# 범위 끝 허용오차(m) — 같은 자리 거리 한 곳(323-7)
|
||||
_PAVED_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
|
||||
def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool:
|
||||
"""이 측점을 포장으로 볼 것인가 — 노면공 · 물넘이 범위 안만 포장, 밖은 비포장(저장값 무시).
|
||||
|
||||
브라우저 짝 = `B06_Section_Cross_Refresh.ts` `pavedBySpecs`(같은 규칙 · 138-1).
|
||||
"""
|
||||
tol = _PAVED_EDGE_TOLERANCE_M
|
||||
return any(start - tol <= chainage_m <= end + tol for start, end in ranges)
|
||||
|
||||
|
||||
# 사용자 조작값 — 다시 계산해도 그대로 승계한다. **이 목록이 서버의 유일한 한 벌이다.**
|
||||
#
|
||||
# 브라우저 쪽 짝은 `B06_Section_Cross_Refresh.ts` 의 `PRESERVED_KEYS` 다(+ `status`·
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""B06 포장 · 세월교 구간 읽기와 판정 — `B06_Section_Router_Design.py` 에서 나눔(472-4 · 동작 같음).
|
||||
|
||||
짝(349-0n): `pavement_ranges` · `ford_surface_drops` · `paved_at` · `ford_drop_at` ↔
|
||||
`B06_Section/logic/B06_Section_Cross_Enforce.ts` · 거울 시험 `test_349_0n_cross_enforce_mirror.py`.
|
||||
부르는 쪽은 `B06_Section_Router_Design` 이름으로 씀(다시 내보냄 · 시험이 그 이름을 갈아 끼움).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 세월교 노면 하강이 걸리는 측점 판정 허용 오차(m) — 세트 부착(`attach_culvert_sets`)과
|
||||
# 같은 기준이라 "구체가 그려진 측점"과 "노면이 내려간 측점"이 어긋나지 않는다.
|
||||
_FORD_DROP_TOLERANCE_M = 0.02
|
||||
|
||||
|
||||
def pavement_suggestions(longitudinal: dict[str, Any]) -> dict[float, bool]:
|
||||
mapping: dict[float, bool] = {}
|
||||
stations = longitudinal.get("stations")
|
||||
for station in stations if isinstance(stations, list) else []:
|
||||
suggested = station.get("pavement_suggested")
|
||||
if isinstance(suggested, bool):
|
||||
mapping[round(float(station.get("chainage_m", 0.0)), 3)] = suggested
|
||||
return mapping
|
||||
|
||||
|
||||
def pavement_ranges(project_root: Path) -> list[tuple[float, float]]:
|
||||
"""포장으로 볼 누가거리 구간 — 구조물 정본 G군 + 물넘이포장(포장 필수).
|
||||
|
||||
포장 지정은 **사용자가 구간으로 준다**(2026-08-28 사용자 확정). 종단경사 자동 판정은
|
||||
더 이상 포장을 켜지 않는다 — 경고 표기(`pavement_suggested`)만 남는다.
|
||||
물넘이포장은 콘크리트 등으로 노면을 만드는 시설이라 범위 안이 항상 포장이다.
|
||||
읽기 실패는 비치명 — 빈 목록이면 사용자 지정이 없는 것과 같다.
|
||||
"""
|
||||
ranges: list[tuple[float, float]] = []
|
||||
try:
|
||||
types = structure_type_map()
|
||||
for structure in load_structures(str(project_root))[1]:
|
||||
definition = types.get(structure.type_id)
|
||||
if definition is None or definition.group != "G":
|
||||
continue
|
||||
anchor = structure.chainage_m
|
||||
start = structure.start_m if structure.start_m is not None else anchor
|
||||
end = structure.end_m if structure.end_m is not None else anchor
|
||||
if start is None or end is None:
|
||||
continue
|
||||
ranges.append((min(float(start), float(end)), max(float(start), float(end))))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 포장 구간을 읽지 못했습니다 (사용자 지정 없음으로 본다)")
|
||||
return ranges + ford_pavement_ranges(project_root)
|
||||
|
||||
|
||||
def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""물넘이포장 범위(기준 ± 월류 폭/2) — 포장 필수 · 측구 없음(286-2). 판정은 `paved_at`."""
|
||||
if project_root is None:
|
||||
return []
|
||||
sets = load_culvert_sets(project_root).items()
|
||||
halves = [
|
||||
(at, float(s.get("span_m") or 0.0) / 2.0)
|
||||
for at, s in sets
|
||||
if s.get("type") == "ford_pavement"
|
||||
]
|
||||
return [(at - half, at + half) for at, half in halves]
|
||||
|
||||
|
||||
def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)].
|
||||
|
||||
구체 위 노면은 월류 높이만큼 낮게 앉는다(2026-08-30 사용자 확정). 계획고를 그만큼
|
||||
내려 잡아야 측벽·바닥판이 맞고 절·성토 면적도 따라온다. 포장 구간과 같은 방식으로
|
||||
관 지점 정본에서 읽는다 — 읽기 실패는 비치명(하강 없음으로 본다).
|
||||
"""
|
||||
if project_root is None:
|
||||
return []
|
||||
drops: list[tuple[float, float]] = []
|
||||
try:
|
||||
for chainage, spec in load_culvert_sets(project_root).items():
|
||||
if spec.get("type") != "ford":
|
||||
continue
|
||||
depth = float(spec.get("overflow_depth_m") or 0.0)
|
||||
if depth > 0:
|
||||
drops.append((chainage, depth))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 세월교 월류 높이를 읽지 못했습니다 (노면 하강 없음으로 본다)")
|
||||
return drops
|
||||
|
||||
|
||||
def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float:
|
||||
"""그 측점의 노면 하강량(m). 구체가 앉은 측점만 — 붙는 기준은 세트 부착과 같다."""
|
||||
for at, depth in drops:
|
||||
if abs(chainage_m - at) <= _FORD_DROP_TOLERANCE_M:
|
||||
return depth
|
||||
return 0.0
|
||||
|
||||
|
||||
# 범위 끝 허용오차(m) — 같은 자리 거리 한 곳(323-7)
|
||||
_PAVED_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
|
||||
def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool:
|
||||
"""이 측점을 포장으로 볼 것인가 — 노면공 · 물넘이 범위 안만 포장, 밖은 비포장(저장값 무시).
|
||||
|
||||
브라우저 짝 = `B06_Section_Cross_Refresh.ts` `pavedBySpecs`(같은 규칙 · 138-1).
|
||||
"""
|
||||
tol = _PAVED_EDGE_TOLERANCE_M
|
||||
return any(start - tol <= chainage_m <= end + tol for start, end in ranges)
|
||||
@@ -9,8 +9,7 @@
|
||||
* 콘크리트 색은 세월교 바닥판 클래스를 그대로 쓴다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import { pipeTagId, tagShape } from "../common_util/common_util_pick_tag";
|
||||
import type { BoxLayout, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
@@ -221,7 +220,7 @@ export function appendBoxOverlay(
|
||||
event.stopPropagation();
|
||||
onSelectSide(role);
|
||||
});
|
||||
tagShape(hit, box.chainage_m != null ? pipeIdAt(box.chainage_m) : null, role); // 신원표(323-5)
|
||||
tagShape(hit, box.chainage_m != null ? pipeTagId(box.chainage_m) : null, role); // 신원표(323-5)
|
||||
hits.set(role, hit);
|
||||
layer.append(hit);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
* 집수정) → ③ 배관 → ④ 유출구측 성토부선(보호공 삭제 — 2026-08-22)
|
||||
* ========================================================================== */
|
||||
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import { pipeTagId, tagShape } from "../common_util/common_util_pick_tag";
|
||||
import {
|
||||
REVET_EMBED_DEPTH_M,
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
@@ -91,7 +90,7 @@ export function appendCulvertOverlay(
|
||||
): RevetHighlightSetter {
|
||||
const { culvert, pipe, pipeCorners } = layout;
|
||||
// 신원표(323-5) — 관 세션 id · 부분 = 조정창 부재키
|
||||
const ownerId = culvert.chainage_m != null ? pipeIdAt(culvert.chainage_m) : null;
|
||||
const ownerId = culvert.chainage_m != null ? pipeTagId(culvert.chainage_m) : null;
|
||||
const diameter = culvert.diameter_m;
|
||||
const roleLabel = (role: "inlet" | "outlet" | "extra") =>
|
||||
role === "inlet" ? "유입" : role === "outlet" ? "유출" : "추가(성토부)";
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { EXTRA_SPAN_DEFAULT } from "./logic/B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { StructureSpan } from "./logic/B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { LocaleKey } from "@ui/ui_template_locale";
|
||||
/**
|
||||
* 기슭막이 4축 조작 제어(2026-08-22 확정) — 벽을 눌러 고르고 조정창에서
|
||||
* 좌우(x)·상하(d, 성토선 대각)·높이(h)·재질(m)을 만진다. 고른 벽은 여기 담아 두어
|
||||
@@ -113,8 +114,12 @@ export interface ExtraWallControl {
|
||||
syncCount: (chainageM: number, built: number, side?: ExtraSide) => void;
|
||||
}
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { L } from "./B06_Section_UI_Section_Common";
|
||||
/** 한계에 잘린 조작 알림 — 화면(`Cross_View`)이 토스트를 꽂음 · 서버 Node 묶음은 안 꽂음(조작 중인 벽이
|
||||
* 없어 부를 일도 없음). 묶음이 화면 코드(토스트 · 글)를 안 끌게 갈라냄(472-2). */
|
||||
let notice: (key: LocaleKey) => void = () => {};
|
||||
export function setCulvertNotice(fn: (key: LocaleKey) => void): void {
|
||||
notice = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배수관 세트 기하 계산 + 제어 상태 동기화.
|
||||
@@ -455,7 +460,7 @@ export function computeCardCulvert(
|
||||
const applied = activeRevet === "inlet" ? layout.revetShift.inlet : layout.revetShift.outlet;
|
||||
const asked = activeRevet === "inlet" ? requested?.inlet : requested?.outlet;
|
||||
if (asked?.d != null && applied.d != null && Math.abs(asked.d - applied.d) > 0.05) {
|
||||
showToast(L("B06_Cross_Revet_Limit_Up"), "info");
|
||||
notice("B06_Cross_Revet_Limit_Up");
|
||||
}
|
||||
}
|
||||
// 종단경사 반영은 **3D 전용** 스위치다(2026-08-24 사용자 확인). 횡단도는 언제나
|
||||
@@ -488,7 +493,7 @@ export function computeCardCulvert(
|
||||
Math.abs(requested.lateralM - applied.lateralM) > 0.05 ||
|
||||
Math.abs(requested.slopeM - applied.slopeM) > 0.05;
|
||||
if (moved) {
|
||||
if (activeRevet === "inlet") showToast(L("B06_Cross_Basin_Limit_Pipe"), "info");
|
||||
if (activeRevet === "inlet") notice("B06_Cross_Basin_Limit_Pipe");
|
||||
inletStructure.updateAdjust(section.chainage_m, applied);
|
||||
}
|
||||
}
|
||||
@@ -518,13 +523,10 @@ export function computeCardCulvert(
|
||||
// 축별로 요청이 잘렸으면 왜 안 됐는지 토스트로 알린다(조작 중인 벽만).
|
||||
if (activeRevet === role) {
|
||||
if (held("x") && Math.abs(requested.x - applied.x) > 0.05) {
|
||||
showToast(
|
||||
L(
|
||||
requested.x < applied.x
|
||||
? "B06_Cross_Revet_Limit_Inward"
|
||||
: "B06_Cross_Revet_Limit_Outward",
|
||||
),
|
||||
"info",
|
||||
notice(
|
||||
requested.x < applied.x
|
||||
? "B06_Cross_Revet_Limit_Inward"
|
||||
: "B06_Cross_Revet_Limit_Outward",
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -533,9 +535,8 @@ export function computeCardCulvert(
|
||||
applied.d != null &&
|
||||
Math.abs(requested.d - applied.d) > 0.05
|
||||
) {
|
||||
showToast(
|
||||
L(requested.d < applied.d ? "B06_Cross_Revet_Limit_Up" : "B06_Cross_Revet_Limit_Down"),
|
||||
"info",
|
||||
notice(
|
||||
requested.d < applied.d ? "B06_Cross_Revet_Limit_Up" : "B06_Cross_Revet_Limit_Down",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
* ④ 계류측 성토부선·절토선. 관 위 성토 채움은 그리지 않는다(2026-08-25 사용자).
|
||||
* ========================================================================== */
|
||||
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import { pipeTagId, tagShape } from "../common_util/common_util_pick_tag";
|
||||
import type { FordLayout, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom";
|
||||
import {
|
||||
appendPipeInfoText,
|
||||
@@ -56,7 +55,7 @@ export function appendFordOverlay(
|
||||
onSelectWall?: (key: FordPick) => void,
|
||||
): FordHighlightSetter {
|
||||
const { ford } = layout;
|
||||
const ownerId = ford.chainage_m != null ? pipeIdAt(ford.chainage_m) : null; // 신원표(323-5)
|
||||
const ownerId = ford.chainage_m != null ? pipeTagId(ford.chainage_m) : null; // 신원표(323-5)
|
||||
const toXY = (point: OffsetPoint): [number, number] => [
|
||||
x(point.offset),
|
||||
toDisplayY(point.elevation),
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
import type { CrossDesign } from "./B06_Section_Api_Fetch";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { appendPavementOverlay } from "./B06_Section_UI_Cross_Design_Surface";
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import { pipeTagId, tagShape } from "../common_util/common_util_pick_tag";
|
||||
import { appendChuteBand, chuteRun } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
|
||||
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
|
||||
@@ -119,7 +118,7 @@ export function appendFordPavementOverlay(
|
||||
.forEach((el) => {
|
||||
el.setAttribute("class", "b06-chart__pavement b06-chart__ford-pavement");
|
||||
// 형상 고름 표식(287-5) — 관 지점 알약 id 와 같은 글
|
||||
if (typeof spec.chainage_m === "number") tagShape(el, pipeIdAt(spec.chainage_m), "concrete"); // 관 세션 id · 부분(323-5)
|
||||
if (typeof spec.chainage_m === "number") tagShape(el, pipeTagId(spec.chainage_m), "concrete"); // 관 세션 id · 부분(323-5)
|
||||
});
|
||||
// 월류 높이선 — 콘크리트 윗면(설계 노면) 위로 월류 높이만큼 · 같은 각도(그림_183) · 성토 쪽 = 성토선 연장(315-2)
|
||||
const depth = spec.depth_m ?? 0;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* 3D 코리도는 `tiers[].polygon`·`span`을 그대로 스윕한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { tagShape } from "../A00_Common/design/design_pick_match";
|
||||
import { tagShape } from "../common_util/common_util_pick_tag";
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
|
||||
@@ -46,7 +46,8 @@ import {
|
||||
} from "./B06_Section_UI_Cross_View_Frame";
|
||||
import { appendFordOverlay, computeFordLayout } from "./B06_Section_UI_Cross_Ford";
|
||||
import type { FordControl } from "./B06_Section_UI_Cross_Ford_Panel";
|
||||
import { computeCardCulvert } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { computeCardCulvert, setCulvertNotice } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import type {
|
||||
ExtraWallControl,
|
||||
@@ -87,6 +88,9 @@ import {
|
||||
effectiveCardHalfWidth,
|
||||
} from "./B06_Section_UI_Cross_View_Metrics";
|
||||
|
||||
// 관 세트 조작이 한계에 잘린 까닭 = 토스트(조작 중인 벽만 · Wire 가 부름).
|
||||
setCulvertNotice((key) => showToast(L(key), "info"));
|
||||
|
||||
/**
|
||||
* 횡단 카드 요소. 선택 표시와 면적 강조를 **카드를 다시 만들지 않고** 갈아 끼우는 핸들을 단다
|
||||
* — 카드를 새로 만들면 사용자가 맞춰 둔 휠 줌·팬(viewBox)이 초기화되기 때문이다.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/* =============================================================================
|
||||
* common_util/common_util_pick_tag.ts
|
||||
* 그린 형상의 신원표(323-5) — 구조물 그림(카드 · 서버 Node 묶음)이 쓰는 몫만.
|
||||
*
|
||||
* 관 id 는 세션 장부(`design_pipe_ids`)가 정함 — 화면이 열 때(`usePipeIds`) `setPipeTagSource` 로 꽂음.
|
||||
* 안 꽂혔으면(서버 Node · 시험) 옛 자리 이름 `pipe-{누가거리}` — 장부가 프로젝트를 모를 때와 같은 값.
|
||||
* 묶음이 세션(`b_page_state`)을 안 끌게 갈라냄(472-2).
|
||||
* ========================================================================== */
|
||||
|
||||
let pipeSource: ((chainageM: number) => string) | null = null;
|
||||
|
||||
/** 관 id 원천 — 화면이 세션 장부를 꽂음(null = 옛 자리 이름). */
|
||||
export function setPipeTagSource(source: ((chainageM: number) => string) | null): void {
|
||||
pipeSource = source;
|
||||
}
|
||||
|
||||
/** 그 자리 관의 신원표 id. */
|
||||
export function pipeTagId(chainageM: number): string {
|
||||
return pipeSource ? pipeSource(chainageM) : `pipe-${chainageM.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** 그린 형상에 신원표를 닮(부분 없으면 통째 형상). */
|
||||
export function tagShape(el: Element, structureId: string | null, part?: string | null): void {
|
||||
if (structureId) el.setAttribute("data-structure-id", structureId);
|
||||
if (part) el.setAttribute("data-part", part);
|
||||
}
|
||||
@@ -31,14 +31,10 @@ const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pa
|
||||
{ "./logic/B06_Section_UI_Cross_Culvert_Const": {} },
|
||||
),
|
||||
"./B06_Section_UI_Cross_Design_Surface": { appendPavementOverlay() {} },
|
||||
// 관 세션 id · 신원표(323-5) — 프로젝트 없음 = 옛 자리 이름
|
||||
"../A00_Common/design/design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}` },
|
||||
"../A00_Common/design/design_pick_match": {
|
||||
tagShape: (el, id, part) => {
|
||||
if (id) el.setAttribute("data-structure-id", id);
|
||||
if (part) el.setAttribute("data-part", part);
|
||||
},
|
||||
},
|
||||
// 관 신원표(323-5 · 472-2 에서 common_util 로) — 진짜 · 장부 안 꽂음 = 옛 자리 이름
|
||||
"../common_util/common_util_pick_tag": loadTs(
|
||||
path.join(ROOT, "common_util", "common_util_pick_tag.ts"),
|
||||
),
|
||||
});
|
||||
// 286-3 도수로 · 돌붙임 길이 — 유출(좌 · 우가 높음) 성토사면 1:1.5(5 m 내려감 · 사면 길이 √(7.5²+5²) ≈ 9.01)
|
||||
const sets = loadTs(path.join(ROOT, "common_util", "common_util_culvert_sets.ts"));
|
||||
|
||||
@@ -26,6 +26,7 @@ if str(ROOT) not in sys.path:
|
||||
import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402
|
||||
import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402
|
||||
import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402
|
||||
import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
@@ -157,7 +158,8 @@ def server(r) -> list[dict]:
|
||||
"""서버 [확정] 차례 그대로 — 저장분(보정 전) → 포장 구간 → 세월교 하강."""
|
||||
patch = pytest.MonkeyPatch()
|
||||
sets = culvert_sets_from(parse_pipe_points(PIPES))
|
||||
for module in (design_mod, tilt_mod):
|
||||
# 구간 읽기는 472-4 에서 `_Router_Design_Ranges` 로 나눔 — 정본 읽기를 그 모듈에서 갈아 끼움.
|
||||
for module in (ranges_mod, tilt_mod):
|
||||
patch.setattr(module, "load_culvert_sets", lambda _root: sets)
|
||||
patch.setattr(module, "load_structures", lambda _root: (0, STRUCTURES))
|
||||
patch.setattr(structures_repo, "load_structures", lambda _root: (0, STRUCTURES))
|
||||
|
||||
@@ -27,6 +27,7 @@ from test_349_0n_cross_enforce_mirror import OFFSETS, _stations, _z # noqa: E40
|
||||
import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402
|
||||
import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402
|
||||
import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402
|
||||
import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
@@ -109,7 +110,8 @@ def server(r) -> list[dict]:
|
||||
"""서버 [확정] — 저장분(입력 없는 측점은 config 암 경계) → 포장 구간 → 세월교 하강."""
|
||||
patch = pytest.MonkeyPatch()
|
||||
sets = culvert_sets_from(parse_pipe_points(PIPES))
|
||||
for module in (design_mod, tilt_mod):
|
||||
# 구간 읽기는 472-4 에서 `_Router_Design_Ranges` 로 나눔 — 정본 읽기를 그 모듈에서 갈아 끼움.
|
||||
for module in (ranges_mod, tilt_mod):
|
||||
patch.setattr(module, "load_culvert_sets", lambda _root: sets)
|
||||
patch.setattr(module, "load_structures", lambda _root: (0, STRUCTURES))
|
||||
patch.setattr(structures_repo, "load_structures", lambda _root: (0, STRUCTURES))
|
||||
|
||||
@@ -47,15 +47,11 @@ pytestmark = pytest.mark.skipif(
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
# 화면 · 세션 끌림(목록 1.3) — 472-2 2-B 가 정리(PIPE_DISPLAY_NAME → common_util · Api_Fetch 세션 ·
|
||||
# 구조물 그림이 고른 것 읽기 · 사진첩).
|
||||
# 화면 · 세션 끌림(목록 1.3) — 472-2 2-B 가 걷음. 남은 줄 = 헬퍼 정규식 헛잡음
|
||||
# (Corridor_Structures_Hash 머리 주석 속 「import」부터 잡아 아래 `import type` 을 값으로 셈) —
|
||||
# 헬퍼를 고치면 빈 목록.
|
||||
SESSION_PULLS = (
|
||||
"A00_Common/b_page_state.ts",
|
||||
"A00_Common/design/design_pick_match.ts",
|
||||
"A00_Common/design/design_pipe_ids.ts",
|
||||
"A00_Common/design/design_selection.ts",
|
||||
"B06_Section/B06_Section_Api_Fetch.ts",
|
||||
"B06_Section/B06_Section_UI_Section_Common.ts",
|
||||
"config/config_frontend.ts",
|
||||
"ui_template/ui_template_elements.ts",
|
||||
"ui_template/ui_template_elements_base.ts",
|
||||
|
||||
Reference in New Issue
Block a user