Merge remote-tracking branches 'origin/main_laptop_1', 'origin/sub_laptop_2' and 'origin/sub_laptop_8' into sub_laptop_3
This commit is contained in:
@@ -37,10 +37,11 @@ import {
|
||||
extraWallRows,
|
||||
structureAreaRows,
|
||||
} from "../../../B06_Section/B06_Section_Structure_Layouts";
|
||||
import { followRunners, type FollowInputs } from "./design_calc_node_follow";
|
||||
|
||||
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
|
||||
|
||||
interface CalcInputs {
|
||||
interface CalcInputs extends FollowInputs {
|
||||
detail?: SectionDetailResponse;
|
||||
route_points?: RoutePoint[];
|
||||
context?: Partial<
|
||||
@@ -136,6 +137,7 @@ function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
let massHaul: MassHaulResult | null = null;
|
||||
|
||||
const RUNNERS: Record<string, () => unknown> = {
|
||||
...followRunners(inputs), // 측점 열쇠 · 따라감 · 꼭짓점 세움(472-4 ①)
|
||||
// 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임.
|
||||
"structure.layouts": () => {
|
||||
const areas = structureAreaRows(sections);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/logic/design_calc_node_follow.ts
|
||||
* 계산 Node 입구(`design_calc_node.ts`)의 따라감 몫(PLAN 472-4 ①) — 측점 열쇠 · 시설 따라감 · 추가 측점 ·
|
||||
* 꼭짓점 세움. 브라우저 묶음이 쓰는 같은 TS 를 부름 · 파이썬은 `common_util_calc_runner.run_calc` 로.
|
||||
*
|
||||
* profile.seat 입력 seat { ground_s · ground_z(저장 선형 표본) · policy(파이썬 `AlignmentPolicy.as_dict`) ·
|
||||
* base_pvi(옮기기 전) · base(옮긴 꼭짓점) · born · held } → 꼭짓점 목록(`seatBornNodes`)
|
||||
* facility.follow 입력 follow { items: [{ placement · chainage_m · start_m · end_m · x · y }] · line ·
|
||||
* max_drift_m · interval_m(site 맞춤 간격) } → [{ item · drift_m · far }](`followStructures`)
|
||||
* facility.plus_follow 입력 plus { rows: [{ chainage_m · x · y }] · line: [[x, y]](새 노선) } →
|
||||
* { rows: [{ chainage_m · x · y }](`followPlusRows` · 정수 m · 같은 m 하나) · moved: 옮긴 수 }
|
||||
* ========================================================================== */
|
||||
|
||||
import { seatBornNodes } from "../../../B05_Profile/logic/B05_Profile_Grade_Seat";
|
||||
import {
|
||||
followPlusRows,
|
||||
followStructures,
|
||||
type FollowingStructure,
|
||||
} from "@util/common_util_route_follow";
|
||||
import type {
|
||||
AlignmentNode,
|
||||
AlignmentPolicy,
|
||||
} from "../../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
|
||||
type Line = Array<[number, number]>;
|
||||
const xyOf = (line: Line) => line.map(([x, y]) => ({ x, y }));
|
||||
|
||||
export interface FollowInputs {
|
||||
follow?: {
|
||||
items: FollowingStructure[];
|
||||
line: Line;
|
||||
max_drift_m?: number;
|
||||
interval_m?: number | null;
|
||||
};
|
||||
plus?: { rows: Array<{ chainage_m: number; x?: number | null; y?: number | null }>; line: Line };
|
||||
seat?: {
|
||||
ground_s: number[];
|
||||
ground_z: number[];
|
||||
policy: AlignmentPolicy;
|
||||
base_pvi: AlignmentNode[];
|
||||
base: AlignmentNode[];
|
||||
born: number[];
|
||||
held?: number[];
|
||||
};
|
||||
}
|
||||
|
||||
/** id → 셈(입구 `RUNNERS` 에 얹음). */
|
||||
export function followRunners(inputs: FollowInputs): Record<string, () => unknown> {
|
||||
return {
|
||||
"profile.seat": () => {
|
||||
const seat = inputs.seat;
|
||||
if (!seat) return null;
|
||||
const base = {
|
||||
chainage: seat.ground_s,
|
||||
ground: seat.ground_z,
|
||||
policy: seat.policy,
|
||||
basePvi: seat.base_pvi,
|
||||
};
|
||||
return seatBornNodes(base, seat.base, seat.born, seat.held ?? []);
|
||||
},
|
||||
"facility.follow": () => {
|
||||
const follow = inputs.follow;
|
||||
if (!follow) return null;
|
||||
return followStructures(
|
||||
follow.items,
|
||||
xyOf(follow.line),
|
||||
follow.max_drift_m,
|
||||
follow.interval_m ?? null,
|
||||
);
|
||||
},
|
||||
"facility.plus_follow": () => {
|
||||
const plus = inputs.plus;
|
||||
if (!plus) return null;
|
||||
const line = xyOf(plus.line);
|
||||
const moved = followStructures(plus.rows, line).filter(
|
||||
({ item }, index) => item.chainage_m !== plus.rows[index].chainage_m,
|
||||
).length;
|
||||
const rows = followPlusRows(plus.rows, line).map(({ chainage_m, x, y }) => ({
|
||||
chainage_m,
|
||||
x: x ?? null,
|
||||
y: y ?? null,
|
||||
}));
|
||||
return { rows, moved };
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -320,11 +320,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "follow",
|
||||
ts: `${B05L}B05_Profile_Grade_Seat.ts#seatBornNodes`,
|
||||
runner: `py:${B05}B05_Profile_Router_Confirm.py#_seat_born_nodes`,
|
||||
until: "472-4",
|
||||
pair: `${B05}B05_Profile_Router_Confirm.py#_seat_born_nodes`,
|
||||
mirror: ["test_427_1_follow_seat", "test_459_route_seat_mirror"],
|
||||
note: "Route_Save `_carry_profile` 도 같은 일",
|
||||
runner: "node:profile.seat",
|
||||
pair: null,
|
||||
mirror: ["test_427_1_follow_seat", "test_459_route_seat_mirror", "test_472_4_follow_node"],
|
||||
note: "파이썬 `Router_Confirm._seat_born_nodes` = Node 부름 겉(측점 병합 · Route_Save `_carry_profile`)",
|
||||
},
|
||||
{
|
||||
id: "profile.tools",
|
||||
@@ -465,10 +464,16 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "follow",
|
||||
ts: `${U}common_util_route_follow.ts#followStructures`,
|
||||
runner: `py:${U}common_util_route_follow.py#follow_structures`,
|
||||
until: "472-4",
|
||||
pair: `${U}common_util_route_follow.py#follow_structures`,
|
||||
mirror: ["test_410_follow_xy", "test_344_2_route_pairs", "test_459_route_seat_mirror"],
|
||||
runner: "node:facility.follow",
|
||||
pair: null,
|
||||
mirror: [
|
||||
"test_410_follow_xy",
|
||||
"test_344_2_route_pairs",
|
||||
"test_459_route_seat_mirror",
|
||||
"test_380_3_site_follow",
|
||||
"test_472_4_follow_node",
|
||||
],
|
||||
note: "파이썬 `common_util_route_follow.follow_structures` = Node 부름 겉 · 저장 때 좌표 맞춤 `fill_structure_coordinates` 는 파이썬 남음(옛 좌표 지킴 · 0.1 mm 반올림 — TS `placeOnLine` 과 규칙 다름)",
|
||||
},
|
||||
{
|
||||
id: "facility.plus_follow",
|
||||
@@ -478,12 +483,11 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
outputs: ["추가 측점(input.plusStations 고침)"],
|
||||
site: "browser_now",
|
||||
stage: "follow",
|
||||
ts: `${B05}B05_Profile_Api_PlusStations.ts#followPlusDraft`,
|
||||
runner: `py:${B05}B05_Profile_Plus_Stations.py#follow_plus_stations`,
|
||||
until: "472-4",
|
||||
pair: `${B05}B05_Profile_Plus_Stations.py#follow_plus_stations`,
|
||||
mirror: ["test_459_route_seat_mirror"],
|
||||
note: "TS 가 Api 파일 안 — logic 으로(472-4 ①)",
|
||||
ts: `${U}common_util_route_follow.ts#followPlusRows`,
|
||||
runner: "node:facility.plus_follow",
|
||||
pair: null,
|
||||
mirror: ["test_459_route_seat_mirror", "test_368_2_plus_stations", "test_472_4_follow_node"],
|
||||
note: "파이썬 `Plus_Stations.follow_plus_stations` = Node 부름 겉(정본 파일 읽기 · 쓰기)",
|
||||
},
|
||||
{
|
||||
id: "facility.stations",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""추가 측점 정본(`B05_Profile/route/plus_stations.json`) — 구조물 정본 곁(368-2 · 요구 240-21).
|
||||
|
||||
사용자가 종단 원지반선에서 넣은 일반 측점(정수 m) · 이름 없음. 자리 정본 = 좌표(x · y) — 노선이
|
||||
바뀌면 관 · 구조물처럼 새 노선에 투영해 누가거리를 다시 매김(`follow_structures` 재사용).
|
||||
바뀌면 관 · 구조물처럼 새 노선에 투영해 누가거리를 다시 매김(TS `followPlusRows` · Node 입구).
|
||||
측점 계획에는 `resolve_extra_stations` → `plan_stations(plus=)` 로 들어감(TS `planStations` 짝).
|
||||
"""
|
||||
|
||||
@@ -10,11 +10,10 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Sequence
|
||||
|
||||
from common_util.common_util_calc_runner import run_calc
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_route_follow import follow_structures
|
||||
|
||||
PLUS_STATIONS_FILE_NAME = "plus_stations.json"
|
||||
|
||||
@@ -71,23 +70,26 @@ def save_plus_stations(
|
||||
|
||||
|
||||
def follow_plus_stations(project_root: str | Path, new_xy: Sequence[Sequence[float]]) -> int:
|
||||
"""노선이 바뀔 때 좌표를 새 노선에 투영해 누가거리를 다시 매김 → 옮긴 수(같은 m 는 하나)."""
|
||||
"""노선이 바뀔 때 좌표를 새 노선에 투영해 누가거리를 다시 매김 → 옮긴 수(같은 m 는 하나).
|
||||
셈 = TS `followPlusRows`(Node 입구 `facility.plus_follow` · 472-4 ①)."""
|
||||
rows = load_plus_stations(project_root)
|
||||
items = [
|
||||
SimpleNamespace(chainage_m=float(r["chainage_m"]), x=r.get("x"), y=r.get("y")) for r in rows
|
||||
]
|
||||
if not items:
|
||||
if not rows:
|
||||
return 0
|
||||
before = [item.chainage_m for item in items]
|
||||
follow_structures(items, new_xy)
|
||||
moved = sum(a != item.chainage_m for a, item in zip(before, items))
|
||||
if moved:
|
||||
seen: set[float] = set()
|
||||
kept = []
|
||||
for item in sorted(items, key=lambda i: i.chainage_m):
|
||||
if item.chainage_m in seen:
|
||||
continue
|
||||
seen.add(item.chainage_m)
|
||||
kept.append({"chainage_m": item.chainage_m, "x": item.x, "y": item.y})
|
||||
atomic_write_json(str(plus_stations_path(project_root)), {"stations": kept})
|
||||
return moved
|
||||
results = run_calc(
|
||||
["facility.plus_follow"],
|
||||
{
|
||||
"plus": {
|
||||
"rows": [
|
||||
{"chainage_m": float(r["chainage_m"]), "x": r.get("x"), "y": r.get("y")}
|
||||
for r in rows
|
||||
],
|
||||
"line": [[float(p[0]), float(p[1])] for p in new_xy],
|
||||
}
|
||||
},
|
||||
)
|
||||
if results is None:
|
||||
raise RuntimeError("추가 측점 따라감 실패 — 계산 Node 입구(facility.plus_follow)")
|
||||
followed = results["facility.plus_follow"]
|
||||
if followed["moved"]:
|
||||
atomic_write_json(str(plus_stations_path(project_root)), {"stations": followed["rows"]})
|
||||
return int(followed["moved"])
|
||||
|
||||
@@ -12,15 +12,12 @@ from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import aiomysql
|
||||
import numpy as np
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Engine_Grade import GradeDesignOptions
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import (
|
||||
AlignmentPolicy,
|
||||
build_curves,
|
||||
chainage_key,
|
||||
evaluate,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
@@ -42,6 +39,7 @@ from B06_Section.B06_Section_Repository import (
|
||||
get_latest_section_options,
|
||||
get_longitudinal_section,
|
||||
)
|
||||
from common_util.common_util_calc_runner import run_calc
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
|
||||
@@ -213,10 +211,6 @@ async def sync_uphill_overrides_into_designs(
|
||||
#: 반올림만 다른 같은 측점(m) — 서버가 관 측점을 0.01m 로 반올림.
|
||||
#: 화면 `applyIrregularStations` 0.006 과 짝.
|
||||
_SAME_STATION_M = 0.006
|
||||
#: 지반 종단으로 볼 꼭짓점 자리 계획선 · 지반 어긋남 가운데값(m) — 새 꼭짓점을 지반에 세울지(438).
|
||||
_SEAT_GROUND_M = 0.01
|
||||
#: 새 꼭짓점 이웃이 옮기기 전 계획고에서 뜨면 맞출 한도(m · 433-5).
|
||||
_SEAT_KEEP_M = 0.001
|
||||
|
||||
|
||||
def _seat_born_nodes(
|
||||
@@ -225,84 +219,36 @@ def _seat_born_nodes(
|
||||
born: list[float],
|
||||
held: list[float] = (),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""새 측점(`born`)에 자동 계획선 규칙(원지반 지남 · `pass_through`)으로 꼭짓점을 세움(427).
|
||||
"""새 측점(`born`)에 자동 계획선 규칙(원지반 지남)으로 꼭짓점을 세움(427 · 433-5 · 438).
|
||||
|
||||
이웃 꼭짓점은 곡선 모양이 바뀌어 옮기기 전 계획고에서 1mm 넘게 뜨면 그 높이로 맞춤(433-5 ·
|
||||
바로 앞뒤부터 번지는 만큼) · 높이 고친 꼭짓점(`held` = 옮긴 뒤 `station_offsets` 측점)과 그
|
||||
밖 꼭짓점은 그대로 · 양 끝 밖 · 이미 꼭짓점 있는(0.006m 안) 자리는 건너뜀. 지반 = 저장
|
||||
선형 표본(`samples`) · 곡선 = 편집 없이(`pass_through` 와 같음). 옮기기 전 계획선이 꼭짓점
|
||||
대부분에서 지반을 안 지나면(균형 종단) 지반 대신 옮기기 전 계획선 높이(438). TS 짝 =
|
||||
`seatBornNodes`(B05_Profile/logic/B05_Profile_Grade_Seat.ts) · 거울 시험 =
|
||||
`test_427_1_follow_seat.py`.
|
||||
셈 = TS `seatBornNodes`(B05_Profile/logic/B05_Profile_Grade_Seat.ts) 한 벌 — 서버 Node 입구
|
||||
`profile.seat`(472-4 ①). 지반 = 저장 선형 표본(`samples`) · 정책 = 파이썬 기본값을 채운 한 벌.
|
||||
세울 자리가 없으면(양 끝 밖 · 이미 꼭짓점 0.006m 안) Node 를 안 부름. 거울 시험 =
|
||||
`test_427_1_follow_seat.py` · `test_472_4_follow_node.py`.
|
||||
"""
|
||||
samples = stored.get("samples") or []
|
||||
if not base or len(samples) < 2:
|
||||
return base
|
||||
first, last = float(base[0]["chainage_m"]), float(base[-1]["chainage_m"])
|
||||
fresh = [
|
||||
value
|
||||
for value in born
|
||||
if first < value < last
|
||||
if not any(
|
||||
first < value < last
|
||||
and all(abs(value - float(node["chainage_m"])) >= _SAME_STATION_M for node in base)
|
||||
]
|
||||
if not fresh:
|
||||
for value in born
|
||||
):
|
||||
return base
|
||||
ground_s = np.array([float(row["chainage_m"]) for row in samples], dtype=np.float64)
|
||||
ground_z = np.array([float(row["ground_elevation_m"]) for row in samples], dtype=np.float64)
|
||||
policy = AlignmentPolicy.from_dict(stored.get("policy") or {})
|
||||
pre_s = np.array([float(node["chainage_m"]) for node in stored["base_pvi"]], dtype=np.float64)
|
||||
pre_z = np.array([float(node["elevation_m"]) for node in stored["base_pvi"]], dtype=np.float64)
|
||||
pre_curves, _warnings = build_curves(pre_s, pre_z, policy)
|
||||
off = evaluate(pre_s, pre_z, pre_curves, pre_s) - np.interp(pre_s, ground_s, ground_z)
|
||||
on_ground = float(np.median(np.abs(off))) < _SEAT_GROUND_M
|
||||
|
||||
def seat_z(value: float) -> float:
|
||||
if on_ground:
|
||||
return float(np.interp(value, ground_s, ground_z))
|
||||
return float(evaluate(pre_s, pre_z, pre_curves, np.array([value]))[0])
|
||||
|
||||
rows = sorted(
|
||||
[(float(node["chainage_m"]), float(node["elevation_m"]), node) for node in base]
|
||||
+ [(value, seat_z(value), None) for value in fresh],
|
||||
key=lambda row: row[0],
|
||||
)
|
||||
base_s = np.array([row[0] for row in rows], dtype=np.float64)
|
||||
born_rows = np.array([row[2] is None for row in rows])
|
||||
# 다른 꼭짓점 목표 = 옮기기 전 계획선 그 자리 높이(433-5) · 맞출 수 있는 = 높이 안 고친 것
|
||||
target = np.where(
|
||||
born_rows,
|
||||
np.array([row[1] for row in rows], dtype=np.float64),
|
||||
evaluate(pre_s, pre_z, pre_curves, base_s),
|
||||
)
|
||||
can = np.array(
|
||||
[
|
||||
row[2] is not None and all(abs(row[0] - value) >= _SAME_STATION_M for value in held)
|
||||
for row in rows
|
||||
]
|
||||
)
|
||||
free = born_rows.copy()
|
||||
base_z = np.array([row[1] for row in rows], dtype=np.float64)
|
||||
for _ in range(len(rows)):
|
||||
for _ in range(20):
|
||||
curves, _warnings = build_curves(base_s, base_z, policy)
|
||||
error = np.where(free, target - evaluate(base_s, base_z, curves, base_s), 0.0)
|
||||
if float(np.max(np.abs(error))) < 1e-6:
|
||||
break
|
||||
base_z = base_z + error
|
||||
# 이웃 곡선 모양이 바뀌어 뜬 꼭짓점 — 그 꼭짓점도 맞춤(번지는 만큼)
|
||||
curves, _warnings = build_curves(base_s, base_z, policy)
|
||||
drift = (
|
||||
can & ~free & (np.abs(target - evaluate(base_s, base_z, curves, base_s)) > _SEAT_KEEP_M)
|
||||
)
|
||||
if not drift.any():
|
||||
break
|
||||
free |= drift
|
||||
return [
|
||||
node
|
||||
if node is not None and not keep
|
||||
else {**(node or {}), "chainage_m": round(float(s), 6), "elevation_m": round(float(z), 6)}
|
||||
for (s, _z, node), z, keep in zip(rows, base_z.tolist(), free.tolist())
|
||||
]
|
||||
seat = {
|
||||
"ground_s": [float(row["chainage_m"]) for row in samples],
|
||||
"ground_z": [float(row["ground_elevation_m"]) for row in samples],
|
||||
"policy": AlignmentPolicy.from_dict(stored.get("policy") or {}).as_dict(),
|
||||
"base_pvi": stored["base_pvi"],
|
||||
"base": base,
|
||||
"born": [float(value) for value in born],
|
||||
"held": [float(value) for value in held],
|
||||
}
|
||||
results = run_calc(["profile.seat"], {"seat": seat})
|
||||
if results is None:
|
||||
raise RuntimeError("꼭짓점 세움 실패 — 계산 Node 입구(profile.seat)")
|
||||
return results["profile.seat"]
|
||||
|
||||
|
||||
def _follow_moved_stations(
|
||||
|
||||
@@ -42,8 +42,8 @@ import { bindDrainageInteractions } from "./B05_Profile_UI_Drainage_Interact";
|
||||
import type { RouteSpanBand } from "./B05_Profile_UI_Drainage_Spans";
|
||||
import { drawDrainageScene } from "./B05_Profile_UI_Drainage_Render";
|
||||
import { computeZoomOutLimit } from "./B05_Profile_UI_Drainage_ZoomRange";
|
||||
import { mountDrainageShow } from "./B05_Profile_UI_Drainage_Show";
|
||||
import {
|
||||
mountDrainageToggles,
|
||||
DRAINAGE_LAYERS,
|
||||
fetchDrainageLayers,
|
||||
createProgressReporter,
|
||||
@@ -159,21 +159,17 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
// 평균 흐름 화살표 — B04가 계산해 둔 것을 그대로 받아 그린다(여기서 계산하지 않는다).
|
||||
let flowArrows: FlowArrow[] = [];
|
||||
let arrowSpacingM = 0;
|
||||
let showArrows = true;
|
||||
// 유입 집중점 — 관 자리를 판단하는 근거. 기본 꺼짐(마커가 관 마커와 겹쳐 읽기 어렵다).
|
||||
let hotspots: Array<{ chainage: number; area: number }> = [];
|
||||
let maxHotspotArea = 0;
|
||||
let showHotspots = false;
|
||||
/** 종단 Z 출처 — 세부유역이 갈리는 근거라 화면에서 확인 가능해야 한다. */
|
||||
let zSource = "";
|
||||
// 유역 안쪽 상류 세류망 — B04가 채택한 기준선을 그대로 받아 강조만 한다.
|
||||
let upstreamLines: Array<Array<[number, number]>> = [];
|
||||
let showUpstream = true;
|
||||
// 계획선 위 유입 강도 색칠 — B04 지도와 같은 색띠·같은 값(2026-08-01 사용자 지시).
|
||||
let strengthSamples: RoutePoint[] = [];
|
||||
let strength: Float64Array<ArrayBuffer> = new Float64Array(0);
|
||||
let maxStrength = 0;
|
||||
let showStrength = true;
|
||||
let scale = 1;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
@@ -187,42 +183,12 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
let canvasHeight = 0;
|
||||
let canvasDpr = 0;
|
||||
|
||||
// 위성사진은 기본 꺼짐(2026-08-18 사용자 지시) — 어두운 사진이 유역 채움색을 묻는다.
|
||||
backgroundImage.hidden = true;
|
||||
mountDrainageToggles(layerButtons, {
|
||||
initial: {
|
||||
satellite: false,
|
||||
arrows: showArrows,
|
||||
strength: showStrength,
|
||||
hotspots: showHotspots,
|
||||
upstream: showUpstream,
|
||||
},
|
||||
onSatellite: (next) => {
|
||||
backgroundImage.hidden = !next;
|
||||
scheduleDraw();
|
||||
},
|
||||
onSheetLayer: (layer, next) => {
|
||||
if (next) activeLayers.add(layer);
|
||||
else activeLayers.delete(layer);
|
||||
scheduleDraw();
|
||||
},
|
||||
onArrows: (next) => {
|
||||
showArrows = next;
|
||||
scheduleDraw();
|
||||
},
|
||||
onStrength: (next) => {
|
||||
showStrength = next;
|
||||
legend.update(maxStrength, showStrength);
|
||||
scheduleDraw();
|
||||
},
|
||||
onHotspots: (next) => {
|
||||
showHotspots = next;
|
||||
scheduleDraw();
|
||||
},
|
||||
onUpstream: (next) => {
|
||||
showUpstream = next;
|
||||
scheduleDraw();
|
||||
},
|
||||
// 표시 토글(위성 · 도엽 · 화살표 · 강도 · 집중점 · 상류) — 상태는 `_Drainage_Show`.
|
||||
const show = mountDrainageShow(layerButtons, {
|
||||
backgroundImage,
|
||||
activeLayers,
|
||||
onStrength: (next) => legend.update(maxStrength, next),
|
||||
redraw: () => scheduleDraw(),
|
||||
});
|
||||
|
||||
function updateImageTransform(): void {
|
||||
@@ -260,17 +226,17 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
activeLayers,
|
||||
routeLayer,
|
||||
upstreamLines,
|
||||
showUpstream,
|
||||
showUpstream: show.upstream,
|
||||
strengthSamples,
|
||||
strength,
|
||||
maxStrength,
|
||||
showStrength,
|
||||
showStrength: show.strength,
|
||||
flowArrows,
|
||||
arrowSpacingM,
|
||||
showArrows,
|
||||
showArrows: show.arrows,
|
||||
hotspots,
|
||||
maxHotspotArea,
|
||||
showHotspots,
|
||||
showHotspots: show.hotspots,
|
||||
pipeEditor,
|
||||
pipeColor,
|
||||
markedChainage,
|
||||
@@ -352,7 +318,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
);
|
||||
strength = built.strength;
|
||||
maxStrength = built.maximum;
|
||||
legend.update(maxStrength, showStrength);
|
||||
legend.update(maxStrength, show.strength);
|
||||
maxHotspotArea = hotspots.reduce((max, spot) => (spot.area > max ? spot.area : max), 0);
|
||||
// 실제 사용된 배관 목록으로 마커를 동기화한다(유역 없는 관 포함).
|
||||
pipeEditor.setPipes(
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Drainage_Show.ts
|
||||
* 배수유역 판 표시 토글 상태 — 위성 · 도엽 · 화살표 · 강도 · 집중점 · 상류 세류
|
||||
* (`B05_Profile_UI_Drainage_Panel.ts` 에서 나눔 · 472-5 · 동작 같음).
|
||||
* ========================================================================== */
|
||||
|
||||
import { mountDrainageToggles, type DrainageLayer } from "./B05_Profile_UI_Drainage_Parts";
|
||||
|
||||
/** 지금 켜진 표시 — 그리기가 읽음(토글이 고침). */
|
||||
export interface DrainageShow {
|
||||
arrows: boolean;
|
||||
/** 유입 강도 색칠 — B04 지도와 같은 색띠·같은 값(2026-08-01 사용자 지시). */
|
||||
strength: boolean;
|
||||
/** 유입 집중점 — 기본 꺼짐(마커가 관 마커와 겹쳐 읽기 어렵다). */
|
||||
hotspots: boolean;
|
||||
/** 유역 안쪽 상류 세류망 — B04 기준선 강조. */
|
||||
upstream: boolean;
|
||||
}
|
||||
|
||||
/** 토글 줄을 붙이고 상태를 돌려줌 — 위성사진은 기본 꺼짐(2026-08-18 사용자 지시). */
|
||||
export function mountDrainageShow(
|
||||
layerButtons: HTMLElement,
|
||||
deps: {
|
||||
backgroundImage: HTMLImageElement;
|
||||
activeLayers: Set<DrainageLayer>;
|
||||
onStrength: (show: boolean) => void;
|
||||
redraw: () => void;
|
||||
},
|
||||
): DrainageShow {
|
||||
const show: DrainageShow = { arrows: true, strength: true, hotspots: false, upstream: true };
|
||||
deps.backgroundImage.hidden = true;
|
||||
mountDrainageToggles(layerButtons, {
|
||||
initial: { satellite: false, ...show },
|
||||
onSatellite: (next) => {
|
||||
deps.backgroundImage.hidden = !next;
|
||||
deps.redraw();
|
||||
},
|
||||
onSheetLayer: (layer, next) => {
|
||||
if (next) deps.activeLayers.add(layer);
|
||||
else deps.activeLayers.delete(layer);
|
||||
deps.redraw();
|
||||
},
|
||||
onArrows: (next) => {
|
||||
show.arrows = next;
|
||||
deps.redraw();
|
||||
},
|
||||
onStrength: (next) => {
|
||||
show.strength = next;
|
||||
deps.onStrength(next);
|
||||
deps.redraw();
|
||||
},
|
||||
onHotspots: (next) => {
|
||||
show.hotspots = next;
|
||||
deps.redraw();
|
||||
},
|
||||
onUpstream: (next) => {
|
||||
show.upstream = next;
|
||||
deps.redraw();
|
||||
},
|
||||
});
|
||||
return show;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import { hiddenOptionKeys } from "./B05_Profile_UI_Structures_Define_Form";
|
||||
import { stationLabel as formatStation } from "../common_util/common_util_station";
|
||||
import { PIPE_MATCH_M, resolveSelection } from "../A00_Common/design/design_selection";
|
||||
import { movedStructure, snapOldStations, snapStationFor } from "./B05_Profile_UI_Structures_Snap";
|
||||
import { followPipeList } from "./B05_Profile_UI_Structures_Panel_Pipes";
|
||||
|
||||
/** 공개 타입·표시 상수는 700줄 제한으로 `_Structures_Panel_Types` 로 옮겼다 —
|
||||
* 옛 임포트 경로가 그대로 동작하도록 여기서 다시 내보낸다(2026-09-04). */
|
||||
@@ -573,36 +574,7 @@ export function createStructuresSection(
|
||||
getStructures: () => [...structures],
|
||||
setPipeFacilities(pipes) {
|
||||
pipeFacilities = pipes.map((pipe) => ({ ...pipe }));
|
||||
if (tempPipeChainage !== null) {
|
||||
// 스냅·재계산으로 좌표가 조금 옮겨진 임시 관을 계속 따라간다.
|
||||
const hit = pipeFacilities.find(
|
||||
(pipe) => Math.abs(pipe.chainage_m - tempPipeChainage!) < PIPE_MATCH_M,
|
||||
);
|
||||
if (hit) {
|
||||
tempPipeChainage = hit.chainage_m;
|
||||
if (selectedPipeChainage !== null) selectedPipeChainage = hit.chainage_m;
|
||||
// 재계산이 끝나 도착한 담당 유역 설계유량을 입력 중인 폼에 반영한다 —
|
||||
// 물넘이·세월교 월류 높이가 이 값으로 계산된다(2026-08-18 진단: 재계산
|
||||
// 후 선택 통지가 조용히 끝나 폼이 유량을 못 받던 문제).
|
||||
facilityOptions.setDesignFlow(hit.design_flow_m3s ?? null);
|
||||
} else {
|
||||
// 임시 배치 관이 재계산에서 사라졌다 — 임시 상태만 정리(관은 이미 없다).
|
||||
tempPipeChainage = null;
|
||||
syncButtons();
|
||||
}
|
||||
}
|
||||
const kept = pipeFacilities.find(
|
||||
(pipe) => Math.abs(pipe.chainage_m - (selectedPipeChainage ?? NaN)) < PIPE_MATCH_M,
|
||||
);
|
||||
if (kept)
|
||||
selectedPipeChainage = kept.chainage_m; // 조금 옮겨진 관도 고름 따라감(233-3)
|
||||
else if (selectedPipeChainage !== null) {
|
||||
// 고르고 있던 관이 사라졌다(삭제·재계산 이동) — 폼도 함께 닫는다.
|
||||
selectedPipeChainage = null;
|
||||
facilityOptions.setFacility(null);
|
||||
syncButtons();
|
||||
}
|
||||
renderList();
|
||||
followPipeList(panelContext, pipeFacilities);
|
||||
},
|
||||
selectPipeByChainage(chainageM) {
|
||||
if (chainageM !== null) callbacks.onReveal?.();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Structures_Panel_Pipes.ts
|
||||
* 「구조물 배치」 폼이 관 목록을 받는 몫 — 임시 관 · 고른 관이 조금 옮겨져도 따라감
|
||||
* (`B05_Profile_UI_Structures_Panel.ts` setPipeFacilities 에서 나눔 · 472-5 · 동작 같음).
|
||||
* ========================================================================== */
|
||||
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import type { StructuresEventContext } from "./B05_Profile_UI_Structures_Panel_Events";
|
||||
import type { PipeFacilityItem } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
|
||||
/** 새 관 목록(`list` = 이미 갈아 낀 폼 목록) — 임시 · 고른 관 누가거리 따라감 · 사라지면 폼 닫음. */
|
||||
export function followPipeList(ctx: StructuresEventContext, list: PipeFacilityItem[]): void {
|
||||
const temp = ctx.tempPipeChainage();
|
||||
if (temp !== null) {
|
||||
// 스냅·재계산으로 좌표가 조금 옮겨진 임시 관을 계속 따라간다.
|
||||
const hit = list.find((pipe) => Math.abs(pipe.chainage_m - temp) < PIPE_MATCH_M);
|
||||
if (hit) {
|
||||
ctx.setTempPipeChainage(hit.chainage_m);
|
||||
if (ctx.selectedPipeChainage() !== null) ctx.setSelectedPipeChainage(hit.chainage_m);
|
||||
// 재계산이 끝나 도착한 담당 유역 설계유량을 입력 중인 폼에 반영한다 —
|
||||
// 물넘이·세월교 월류 높이가 이 값으로 계산된다(2026-08-18 진단: 재계산
|
||||
// 후 선택 통지가 조용히 끝나 폼이 유량을 못 받던 문제).
|
||||
ctx.facilityOptions.setDesignFlow(hit.design_flow_m3s ?? null);
|
||||
} else {
|
||||
// 임시 배치 관이 재계산에서 사라졌다 — 임시 상태만 정리(관은 이미 없다).
|
||||
ctx.setTempPipeChainage(null);
|
||||
ctx.syncButtons();
|
||||
}
|
||||
}
|
||||
const kept = list.find(
|
||||
(pipe) => Math.abs(pipe.chainage_m - (ctx.selectedPipeChainage() ?? NaN)) < PIPE_MATCH_M,
|
||||
);
|
||||
if (kept)
|
||||
ctx.setSelectedPipeChainage(kept.chainage_m); // 조금 옮겨진 관도 고름 따라감(233-3)
|
||||
else if (ctx.selectedPipeChainage() !== null) {
|
||||
// 고르고 있던 관이 사라졌다(삭제·재계산 이동) — 폼도 함께 닫는다.
|
||||
ctx.setSelectedPipeChainage(null);
|
||||
ctx.facilityOptions.setFacility(null);
|
||||
ctx.syncButtons();
|
||||
}
|
||||
ctx.renderList();
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 바뀌어 옮기기 전 계획고에서 1mm 넘게 뜨면 그 높이로 맞춤(433-5 · 바로 앞뒤부터 번지는 만큼) · 높이 고친 꼭짓점은 그대로.
|
||||
* 옮기기 전 계획선이 꼭짓점 대부분에서 지반을 안 지나면(균형 종단) 지반 대신 옮기기 전 계획선 높이(438).
|
||||
*
|
||||
* 짝 = 파이썬 `B05_Profile_Router_Confirm._seat_born_nodes` · 세울 측점은 공용 `followMovedNodes`
|
||||
* 서버도 이 셈 — Node 입구 `profile.seat`(472-4 ① · 파이썬 `Router_Confirm._seat_born_nodes` 가 부름) · 세울 측점은 공용 `followMovedNodes`
|
||||
* (common_util_structure_station.ts)가 고름 · 거울 시험 = resources/tester/test_427_1_follow_seat.py.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
type AlignmentNode,
|
||||
} from "./B05_Profile_UI_Profile_Alignment";
|
||||
|
||||
/** 지반 종단으로 볼 꼭짓점 자리 계획선 · 지반 어긋남 가운데값(m) — 파이썬 `_SEAT_GROUND_M`. */
|
||||
/** 지반 종단으로 볼 꼭짓점 자리 계획선 · 지반 어긋남 가운데값(m) (438). */
|
||||
const SEAT_GROUND_M = 0.01;
|
||||
/** 새 꼭짓점 이웃이 옮기기 전 계획고에서 뜨면 맞출 한도(m · 433-5) — 파이썬 `_SEAT_KEEP_M`. */
|
||||
/** 새 꼭짓점 이웃이 옮기기 전 계획고에서 뜨면 맞출 한도(m · 433-5). */
|
||||
const SEAT_KEEP_M = 0.001;
|
||||
|
||||
/** `np.median` 짝. */
|
||||
|
||||
@@ -13,11 +13,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { PipeFacilityItem } from "../A00_Common/b_structures_section";
|
||||
import {
|
||||
bindDesignView,
|
||||
sharedStructures,
|
||||
type DesignPageShared,
|
||||
} from "../A00_Common/design/design_shared";
|
||||
import { bindDesignView, sharedStructures } from "../A00_Common/design/design_shared";
|
||||
import {
|
||||
fetchStructureTypes,
|
||||
staleDefinitionNotice,
|
||||
@@ -58,10 +54,17 @@ import {
|
||||
resetAdjustDock,
|
||||
setAdjustSideSlots,
|
||||
} from "./B06_Section_UI_Adjust_Dock";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec";
|
||||
import { listPick } from "../common_util/common_util_structure_catalog";
|
||||
import { followMovedPipe, pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import type {
|
||||
B06StructuresPanel,
|
||||
B06StructuresPanelDeps,
|
||||
} from "./B06_Section_UI_Page_Structures_Panel_Types";
|
||||
export type {
|
||||
B06StructuresPanel,
|
||||
B06StructuresPanelDeps,
|
||||
} from "./B06_Section_UI_Page_Structures_Panel_Types";
|
||||
|
||||
const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요.";
|
||||
const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다.";
|
||||
@@ -74,96 +77,6 @@ const PIPE_PICK_GUIDE = "먼저 횡단도나 목록에서 구조물을 고르세
|
||||
const PIPE_MOVE_NOTICE =
|
||||
"기준 측점을 옮겼습니다 — 횡단도는 바로 보이고, 세부 배수유역은 뒤에서 다시 나눕니다.";
|
||||
|
||||
export interface B06StructuresPanelDeps {
|
||||
projectId: string | null;
|
||||
/** 한 페이지 한 벌 — 「구조물 배치」 폼은 종단 보기와 같은 노드다(계획서 24-3b). */
|
||||
shared: DesignPageShared;
|
||||
/** 횡단 캐시 — 폼 기본값을 조정창과 **같은 원천**(culvert 스펙)에서 읽는다
|
||||
* (2026-08-29 사용자: 예전 조정창이 보여주던 값이 기본값이다). */
|
||||
detail?: () => SectionDetailResponse | null;
|
||||
/** 지금 그려진 벽 높이(m) — 배관 벽은 스펙이 아니라 기하가 정하므로 폼 기본
|
||||
* 높이도 이 값을 쓴다(조정창 표시값과 같다). */
|
||||
wallHeight?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => number | null;
|
||||
/** 지금 그려진 벽 형태 — 조정값(m)이 있으면 그쪽이 정본이라 폼도 이 값을 보인다. */
|
||||
wallForm?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => string | null;
|
||||
/** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */
|
||||
wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null;
|
||||
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
|
||||
stationInterval: () => number;
|
||||
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다(revealOnly = 고르지도 스크롤도 않음 · 측점 없는 구조물 · 307). */
|
||||
focusChainage: (chainageM: number, revealOnly?: boolean) => void;
|
||||
/** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */
|
||||
releaseCard?: () => void;
|
||||
/** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름 · span 끔 = 범위 테두리 없이 · 137-4). */
|
||||
markStructure?: (structureId: string | null, span?: boolean) => void;
|
||||
/** 목록이 바뀔 때마다 종단 알약 레인에 같은 목록을 넘긴다(2026-09-07 표시 통일). */
|
||||
/** `pipes` = 관 지점(구간 포함) — 종단 띠 측점 계획(322-3). */
|
||||
onMarks?: (
|
||||
structures: StructureInstance[],
|
||||
types: StructureType[],
|
||||
pipes: ReadonlyArray<PipeFacilityItem>,
|
||||
) => void;
|
||||
/** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 —
|
||||
* 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */
|
||||
applyPipeOptions?: (chainageM: number, patch: Record<string, number | string>) => void;
|
||||
/** 관 목록 초안을 고쳤다 — 한 벌 배수유역도가 그 초안으로 다시 선다. */
|
||||
onPipesWritten?: () => void;
|
||||
}
|
||||
|
||||
export interface B06StructuresPanel {
|
||||
/** 「구조물 배치」 섹션(하위 [횡단 조정] 포함) — 좌측 패널에 붙인다. */
|
||||
root: HTMLElement;
|
||||
/** 배치된 구조물 목록 — 하단 고정 dock에 붙인다(B05와 같은 자리). */
|
||||
listRoot: HTMLElement;
|
||||
/** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */
|
||||
load: () => Promise<void>;
|
||||
/** 가게(초안 · 없으면 저장분) 관 · 구조물 자리가 목록과 다른가 — 다시 실을 때(노선 초안 따라감 · 평면 마커 옮김 · 440). */
|
||||
listStale: () => boolean;
|
||||
/** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인
|
||||
* 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */
|
||||
overrideOptions: (chainageM: number, patch: Record<string, string | number>) => void;
|
||||
/** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고,
|
||||
* 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). 횡단도의 카드 ·
|
||||
* 벽 · 구체 어디를 눌러도 이 길 하나다. 폼이 연 것을 고름 값으로 돌려준다. */
|
||||
showAtChainage: (chainageM: number | null) => DesignSelection;
|
||||
/** 횡단도에서 벽·구체 선택을 풀 때 — 관 계열 폼만 비운다. 구조물 폼은 그대로 둔다(카드를
|
||||
* 갈아 선택하며 옛 카드의 벽 선택이 풀리는 순간 새 카드의 폼을 지우면 안 된다). */
|
||||
releasePipe: () => void;
|
||||
/** 그 측점에서 폼이 열 항목을 **열지 않고** 고름 값으로 풀어 준다(세션에 적는 데 쓴다). */
|
||||
selectionAt: (chainageM: number | null) => DesignSelection;
|
||||
/** 다음 카드 고름이 측점보다 먼저 열 구조물 — 알약 · 넘김값이 누른 것(null = 측점 규칙대로). */
|
||||
prefer: (structureId: string | null) => void;
|
||||
/** 지금 카드 고름이 먼저 열 구조물(목록 · 알약 · 넘김) — 없으면 null(카드만 고름 · 189-1). */
|
||||
preferred: () => string | null;
|
||||
/** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */
|
||||
selectStructure: (structureId: string) => void;
|
||||
/** 측점 없는 관 하나를 폼에 올림 — 카드 안 고름 · 스크롤 없음 · 알약 · 고름 값에 알약 id(요구 178 · 307). */
|
||||
selectPipeAt: (chainageM: number) => void;
|
||||
/** 넘김값(B05 알약 · 3D · 지도)의 카드 고름 — `focus` 가 그 카드를 고름 · 넘긴 구조물이 먼저(52-15) ·
|
||||
* 폼은 id 로 직접 엶(53-1) · 측점 없는 구조물은 카드 고름 · 스크롤 없이 폼만(요구 178 · 307). */
|
||||
focusPicked: (structureId: string | null, focus: () => void) => void;
|
||||
/** 배수유역도가 관 초안에 채운 빈 월류 높이(126-1)를 목록 · 폼에 받는다(298-1). */
|
||||
adoptFordHeights: () => void;
|
||||
/** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */
|
||||
getStructures: () => StructureInstance[];
|
||||
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */
|
||||
currentChainage: () => number | null;
|
||||
/** 종단 그래프 우클릭이 쓰는 길 — 좌측 폼·목록과 **같은 함수**를 탄다(2026-09-12). */
|
||||
addStructureAt: (chainageM: number, typeId: string) => void;
|
||||
removeStructureById: (structureId: string) => boolean;
|
||||
addPipeAt: (chainageM: number) => void;
|
||||
removePipeAt: (chainageM: number) => void;
|
||||
movePipeTo: (fromChainageM: number, toChainageM: number) => void;
|
||||
moveStructureTo: (structureId: string, toChainageM: number) => boolean;
|
||||
/** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */
|
||||
facility: {
|
||||
setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void;
|
||||
onInletStructureChange: (handler: (value: "auto" | "revet" | "I" | "L" | "U") => void) => void;
|
||||
/** 횡단 단 수가 바뀜 — 배관 흙막이 컨테이너 수를 맞춤(54-1). */
|
||||
recount: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
type PlaceOf = { chainage_m?: number | null; start_m?: number | null; end_m?: number | null };
|
||||
/** 목록 자리 지문 — 누가거리 · 구간만(옵션 고침은 안 셈 · 440). */
|
||||
const placeKey = (list: ReadonlyArray<PlaceOf>): string =>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Page_Structures_Panel_Types.ts
|
||||
* 횡단 보기 「구조물 배치」 몫의 타입 — `B06_Section_UI_Page_Structures_Panel.ts` 에서 나눔
|
||||
* (472-5 · 700줄 · 동작 같음 · 그쪽이 다시 내보냄).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { PipeFacilityItem } from "../A00_Common/b_structures_section";
|
||||
import type { DesignPageShared } from "../A00_Common/design/design_shared";
|
||||
import type { DesignSelection } from "../A00_Common/design/design_selection";
|
||||
import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
|
||||
export interface B06StructuresPanelDeps {
|
||||
projectId: string | null;
|
||||
/** 한 페이지 한 벌 — 「구조물 배치」 폼은 종단 보기와 같은 노드다(계획서 24-3b). */
|
||||
shared: DesignPageShared;
|
||||
/** 횡단 캐시 — 폼 기본값을 조정창과 **같은 원천**(culvert 스펙)에서 읽는다
|
||||
* (2026-08-29 사용자: 예전 조정창이 보여주던 값이 기본값이다). */
|
||||
detail?: () => SectionDetailResponse | null;
|
||||
/** 지금 그려진 벽 높이(m) — 배관 벽은 스펙이 아니라 기하가 정하므로 폼 기본
|
||||
* 높이도 이 값을 쓴다(조정창 표시값과 같다). */
|
||||
wallHeight?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => number | null;
|
||||
/** 지금 그려진 벽 형태 — 조정값(m)이 있으면 그쪽이 정본이라 폼도 이 값을 보인다. */
|
||||
wallForm?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => string | null;
|
||||
/** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */
|
||||
wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null;
|
||||
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
|
||||
stationInterval: () => number;
|
||||
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다(revealOnly = 고르지도 스크롤도 않음 · 측점 없는 구조물 · 307). */
|
||||
focusChainage: (chainageM: number, revealOnly?: boolean) => void;
|
||||
/** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */
|
||||
releaseCard?: () => void;
|
||||
/** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름 · span 끔 = 범위 테두리 없이 · 137-4). */
|
||||
markStructure?: (structureId: string | null, span?: boolean) => void;
|
||||
/** 목록이 바뀔 때마다 종단 알약 레인에 같은 목록을 넘긴다(2026-09-07 표시 통일). */
|
||||
/** `pipes` = 관 지점(구간 포함) — 종단 띠 측점 계획(322-3). */
|
||||
onMarks?: (
|
||||
structures: StructureInstance[],
|
||||
types: StructureType[],
|
||||
pipes: ReadonlyArray<PipeFacilityItem>,
|
||||
) => void;
|
||||
/** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 —
|
||||
* 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */
|
||||
applyPipeOptions?: (chainageM: number, patch: Record<string, number | string>) => void;
|
||||
/** 관 목록 초안을 고쳤다 — 한 벌 배수유역도가 그 초안으로 다시 선다. */
|
||||
onPipesWritten?: () => void;
|
||||
}
|
||||
|
||||
export interface B06StructuresPanel {
|
||||
/** 「구조물 배치」 섹션(하위 [횡단 조정] 포함) — 좌측 패널에 붙인다. */
|
||||
root: HTMLElement;
|
||||
/** 배치된 구조물 목록 — 하단 고정 dock에 붙인다(B05와 같은 자리). */
|
||||
listRoot: HTMLElement;
|
||||
/** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */
|
||||
load: () => Promise<void>;
|
||||
/** 가게(초안 · 없으면 저장분) 관 · 구조물 자리가 목록과 다른가 — 다시 실을 때(노선 초안 따라감 · 평면 마커 옮김 · 440). */
|
||||
listStale: () => boolean;
|
||||
/** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인
|
||||
* 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */
|
||||
overrideOptions: (chainageM: number, patch: Record<string, string | number>) => void;
|
||||
/** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고,
|
||||
* 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). 횡단도의 카드 ·
|
||||
* 벽 · 구체 어디를 눌러도 이 길 하나다. 폼이 연 것을 고름 값으로 돌려준다. */
|
||||
showAtChainage: (chainageM: number | null) => DesignSelection;
|
||||
/** 횡단도에서 벽·구체 선택을 풀 때 — 관 계열 폼만 비운다. 구조물 폼은 그대로 둔다(카드를
|
||||
* 갈아 선택하며 옛 카드의 벽 선택이 풀리는 순간 새 카드의 폼을 지우면 안 된다). */
|
||||
releasePipe: () => void;
|
||||
/** 그 측점에서 폼이 열 항목을 **열지 않고** 고름 값으로 풀어 준다(세션에 적는 데 쓴다). */
|
||||
selectionAt: (chainageM: number | null) => DesignSelection;
|
||||
/** 다음 카드 고름이 측점보다 먼저 열 구조물 — 알약 · 넘김값이 누른 것(null = 측점 규칙대로). */
|
||||
prefer: (structureId: string | null) => void;
|
||||
/** 지금 카드 고름이 먼저 열 구조물(목록 · 알약 · 넘김) — 없으면 null(카드만 고름 · 189-1). */
|
||||
preferred: () => string | null;
|
||||
/** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */
|
||||
selectStructure: (structureId: string) => void;
|
||||
/** 측점 없는 관 하나를 폼에 올림 — 카드 안 고름 · 스크롤 없음 · 알약 · 고름 값에 알약 id(요구 178 · 307). */
|
||||
selectPipeAt: (chainageM: number) => void;
|
||||
/** 넘김값(B05 알약 · 3D · 지도)의 카드 고름 — `focus` 가 그 카드를 고름 · 넘긴 구조물이 먼저(52-15) ·
|
||||
* 폼은 id 로 직접 엶(53-1) · 측점 없는 구조물은 카드 고름 · 스크롤 없이 폼만(요구 178 · 307). */
|
||||
focusPicked: (structureId: string | null, focus: () => void) => void;
|
||||
/** 배수유역도가 관 초안에 채운 빈 월류 높이(126-1)를 목록 · 폼에 받는다(298-1). */
|
||||
adoptFordHeights: () => void;
|
||||
/** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */
|
||||
getStructures: () => StructureInstance[];
|
||||
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */
|
||||
currentChainage: () => number | null;
|
||||
/** 종단 그래프 우클릭이 쓰는 길 — 좌측 폼·목록과 **같은 함수**를 탄다(2026-09-12). */
|
||||
addStructureAt: (chainageM: number, typeId: string) => void;
|
||||
removeStructureById: (structureId: string) => boolean;
|
||||
addPipeAt: (chainageM: number) => void;
|
||||
removePipeAt: (chainageM: number) => void;
|
||||
movePipeTo: (fromChainageM: number, toChainageM: number) => void;
|
||||
moveStructureTo: (structureId: string, toChainageM: number) => boolean;
|
||||
/** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */
|
||||
facility: {
|
||||
setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void;
|
||||
onInletStructureChange: (handler: (value: "auto" | "revet" | "I" | "L" | "U") => void) => void;
|
||||
/** 횡단 단 수가 바뀜 — 배관 흙막이 컨테이너 수를 맞춤(54-1). */
|
||||
recount: () => void;
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,9 @@ NODE_IDS = frozenset(
|
||||
"haul.plan",
|
||||
"excavation.trench",
|
||||
"corridor.mesh",
|
||||
"profile.seat",
|
||||
"facility.plus_follow",
|
||||
"facility.follow",
|
||||
"cross.design",
|
||||
"cross.berm_spans",
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
- `fill_structure_coordinates` — 저장 때 노선 위 기준 자리로 x · y 채움(자리가 그대로면 옛 좌표
|
||||
지킴 · 보낸 좌표가 그 누가거리를 낳으면 살림 · 410) · 옛 자료는 읽을 때 없는 것만 채움.
|
||||
- `follow_structures` — 좌표 → 새 노선 누가거리 · 구간 끝은 같은 만큼 밂 · 노선과의 거리.
|
||||
- `follow_structures` — 좌표 → 새 노선 누가거리 · 구간 끝은 같은 만큼 밂 · 노선과의 거리
|
||||
(셈 = TS `followStructures` · Node 입구 `facility.follow` · 472-4 ①).
|
||||
|
||||
- `route_outside` — 노선 밖(시점 앞 · 종점 뒤 0.51 m 넘음) · 측점 세울 때 셈(459 · 정본에 안 씀).
|
||||
|
||||
@@ -22,15 +23,17 @@ from typing import Any, Iterable, Sequence
|
||||
|
||||
from shapely.geometry import LineString, Point
|
||||
|
||||
from common_util.common_util_calc_runner import run_calc
|
||||
from common_util.common_util_structure_station import (
|
||||
SAME_STATION_M,
|
||||
route_general_stations,
|
||||
snap_existing_station,
|
||||
snap_structure_station,
|
||||
)
|
||||
from config.config_system import STRUCTURE_ROUTE_DRIFT_MAX_M
|
||||
from config.config_system import SECTION_STATION_INTERVAL_M, STRUCTURE_ROUTE_DRIFT_MAX_M
|
||||
|
||||
_POSITION_KEYS = ("chainage_m", "start_m", "end_m")
|
||||
_FOLLOW_KEYS = ("placement", *_POSITION_KEYS, "x", "y")
|
||||
#: 노선 밖 판정(m) — 투영이 시점 앞 · 종점 뒤로 이만큼 넘으면 측점 안 세움(459 · TS 짝).
|
||||
ROUTE_OUTSIDE_M = 0.51
|
||||
|
||||
@@ -125,33 +128,31 @@ def follow_structures(
|
||||
"""좌표를 새 노선에 투영해 누가거리를 다시 매긴다(제자리) → 항목마다 (노선과 거리, 멀어짐).
|
||||
|
||||
기준 자리 = 투영 → 정수 m(노선 끝 안) · 구간 끝(`start_m` · `end_m`)은 같은 만큼 밀고 0 ~ 연장
|
||||
안으로 자름. 좌표가 없는 옛 자료는 그대로(거리 None).
|
||||
site 꼴(사토장 · 토취장 · 새 측점 안 심음) = 새 노선 일반 측점 중 가장 가까운 곳(380-3).
|
||||
안으로 자름. 좌표가 없는 옛 자료는 그대로(거리 None) · site 꼴(사토장 · 토취장) = 새 노선 일반
|
||||
측점 중 가장 가까운 곳(380-3). 셈 = TS `followStructures`(Node 입구 `facility.follow` · 472-4).
|
||||
"""
|
||||
items = list(items)
|
||||
line = _line(xy)
|
||||
if line is None:
|
||||
if line is None or not items:
|
||||
return [(None, False)] * len(items)
|
||||
length = float(line.length)
|
||||
result: list[tuple[float | None, bool]] = []
|
||||
for item in items:
|
||||
x, y = getattr(item, "x", None), getattr(item, "y", None)
|
||||
anchor = getattr(item, "chainage_m", None)
|
||||
if x is None or y is None or anchor is None:
|
||||
result.append((None, False))
|
||||
continue
|
||||
point = Point(float(x), float(y))
|
||||
at = float(line.project(point))
|
||||
target = (
|
||||
snap_existing_station(at, route_general_stations(length))
|
||||
if getattr(item, "placement", None) == "site"
|
||||
else snap_structure_station(at, [length])
|
||||
)
|
||||
shift = target - float(anchor)
|
||||
rows = [{key: getattr(item, key, None) for key in _FOLLOW_KEYS} for item in items]
|
||||
results = run_calc(
|
||||
["facility.follow"],
|
||||
{
|
||||
"follow": {
|
||||
"items": rows,
|
||||
"line": [[x, y] for x, y in line.coords],
|
||||
"max_drift_m": max_drift_m,
|
||||
"interval_m": SECTION_STATION_INTERVAL_M,
|
||||
}
|
||||
},
|
||||
)
|
||||
if results is None:
|
||||
raise RuntimeError("구조물 따라감 실패 — 계산 Node 입구(facility.follow)")
|
||||
out: list[tuple[float | None, bool]] = []
|
||||
for item, row in zip(items, results["facility.follow"], strict=True):
|
||||
for key in _POSITION_KEYS:
|
||||
value = getattr(item, key, None)
|
||||
if value is not None:
|
||||
setattr(item, key, min(max(float(value) + shift, 0.0), length))
|
||||
drift = float(line.distance(point))
|
||||
result.append((drift, drift > max_drift_m))
|
||||
return result
|
||||
if getattr(item, key, None) is not None:
|
||||
setattr(item, key, float(row["item"][key]))
|
||||
out.append((row["drift_m"], bool(row["far"])))
|
||||
return out
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* - `placeOnLine` 의 좌표 없는 관 · `rebasePipeDraft` 는 TS 만 — 좌표 없는 세션 관 초안(정본 PUT 꼴)을 따라가기
|
||||
* 전 자리로 · 노선을 저장한 [저장]이 서버가 따라 옮긴 관 위에 초안을 얹음(349-0u).
|
||||
* - `rebaseStructureDraft` 도 TS 만 — 구조물 초안을 서버가 따라 옮긴 정본 위에(459 · 관 짝).
|
||||
* - `followPlusStations` ↔ 파이썬 `B05_Profile_Plus_Stations.follow_plus_stations`(459 · 노선 초안 동안).
|
||||
* - `followPlusRows` = 추가 측점 따라감 한 벌 — 서버도(Node 입구 `facility.plus_follow` · 472-4 ①) ·
|
||||
* `followPlusStations` = 노선 초안 동안(옛 선 위 자리에서).
|
||||
* - `routeOutside` ↔ 파이썬 `route_outside`(459 · 노선 밖 = 측점 계획 · 알약에서 뺌 · 좌표 그대로) — 따라감이
|
||||
* `outside: true` 를 붙임(파생값 · 저장 때 뺌 · 서버는 측점 세울 때 다시 셈).
|
||||
* 거울 시험 = resources/tester/test_344_2_route_pairs.py.
|
||||
@@ -221,8 +222,21 @@ export function rebaseStructureDraft<S extends StructureLike>(
|
||||
});
|
||||
}
|
||||
|
||||
/** 땅 위 자리(x · y)가 있는 추가 측점이 새 선(`to`)을 따라감 — 정수 m · 누가거리 순 · 같은 m 하나(앞엣것) ·
|
||||
* 좌표 그대로. 서버 정본(`plus_stations.json`)도 이 셈(Node 입구 `facility.plus_follow` · 472-4 ①). */
|
||||
export function followPlusRows<R extends FollowingStructure>(
|
||||
rows: ReadonlyArray<R>,
|
||||
to: ReadonlyArray<XY>,
|
||||
): R[] {
|
||||
const seen = new Set<number>();
|
||||
return followStructures(rows, to)
|
||||
.map(({ item }) => item)
|
||||
.sort((a, b) => a.chainage_m! - b.chainage_m!)
|
||||
.filter((item) => !seen.has(item.chainage_m!) && seen.add(item.chainage_m!));
|
||||
}
|
||||
|
||||
/** 추가 측점이 노선을 따라감(459) — 옛 선(`from`) 위 누가거리의 땅 위 자리를 새 선(`to`)에 투영한 정수 m
|
||||
* (같은 m 하나 · 오름차순). 파이썬 `follow_plus_stations` 짝(`follow_structures` 같은 셈). */
|
||||
* (같은 m 하나 · 오름차순 · 노선 초안 동안 = 좌표가 아직 옛 선 위). */
|
||||
export function followPlusStations(
|
||||
list: ReadonlyArray<number>,
|
||||
from: ReadonlyArray<XY>,
|
||||
@@ -232,8 +246,7 @@ export function followPlusStations(
|
||||
list.map((chainage_m) => ({ chainage_m })),
|
||||
from,
|
||||
);
|
||||
const moved = followStructures(placed, to).map(({ item }) => item.chainage_m!);
|
||||
return [...new Set(moved)].sort((a, b) => a - b);
|
||||
return followPlusRows(placed, to).map((item) => item.chainage_m!);
|
||||
}
|
||||
|
||||
/** 따라가기만 한 관 = 정본과 같은 땅 위 자리(m) — 서버 좌표가 mm 3자리라 그 반올림 폭. */
|
||||
|
||||
@@ -99,7 +99,9 @@ def test_배선() -> None:
|
||||
listing = _read("B05_Profile", "B05_Profile_UI_Structures_List.ts")
|
||||
assert "< PIPE_MATCH_M" in listing and "< 0.05" not in listing
|
||||
structures = _read("B05_Profile", "B05_Profile_UI_Structures_Panel.ts")
|
||||
assert "selectedPipeChainage = kept.chainage_m;" in structures
|
||||
# 관 목록 받기는 472-5 에서 `_Structures_Panel_Pipes` 로 나눔(동작 같음)
|
||||
pipes = _read("B05_Profile", "B05_Profile_UI_Structures_Panel_Pipes.ts")
|
||||
assert "ctx.setSelectedPipeChainage(kept.chainage_m);" in pipes
|
||||
# 저장 · 새로 읽기 뒤 목록 주입이 관 폼을 닫지 않음(닫히며 종단 선까지 꺼졌음)
|
||||
assert "if (!applyingLive && selectedPipeChainage === null)" in structures
|
||||
assert len(structures.splitlines()) <= 700
|
||||
|
||||
@@ -40,6 +40,9 @@ NODE_IDS = {
|
||||
"haul.plan",
|
||||
"excavation.trench",
|
||||
"corridor.mesh",
|
||||
"profile.seat",
|
||||
"facility.plus_follow",
|
||||
"facility.follow",
|
||||
"cross.design",
|
||||
"cross.berm_spans",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""PLAN 472-4 ① — 측점 열쇠 · 시설 따라감 · 추가 측점 · 꼭짓점 세움 = 서버도 같은 TS(Node 입구).
|
||||
|
||||
파이썬 짝을 걷고 서버 부름 자리는 `common_util_calc_runner.run_calc`(등록부 id) 한 길 —
|
||||
profile.seat `Router_Confirm._seat_born_nodes`(측점 병합 · 노선 저장 `_carry_profile`)
|
||||
facility.plus_follow `Plus_Stations.follow_plus_stations`(노선 저장)
|
||||
고정 자료 = `fixtures/golden_472/detail.json`(472-1 · 읽기만).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_4_follow_node.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile import B05_Profile_Router_Confirm as confirm # noqa: E402
|
||||
from common_util import common_util_calc_runner as runner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
DETAIL = json.loads((HERE / "fixtures" / "golden_472" / "detail.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calls(monkeypatch) -> list[list[str]]:
|
||||
seen: list[list[str]] = []
|
||||
|
||||
def spy(ids, inputs):
|
||||
seen.append(list(ids))
|
||||
return runner.run_calc(ids, inputs)
|
||||
|
||||
monkeypatch.setattr(confirm, "run_calc", spy)
|
||||
return seen
|
||||
|
||||
|
||||
def test_꼭짓점_세움은_Node_입구(calls) -> None:
|
||||
stored = DETAIL["longitudinal"]["profile_alignment"]
|
||||
base = [node for node in stored["base_pvi"] if abs(node["chainage_m"] - 351.0) > 0.5]
|
||||
got = confirm._seat_born_nodes(stored, base, [351.0])
|
||||
assert calls == [["profile.seat"]]
|
||||
assert len(got) == len(base) + 1
|
||||
seated = next(node for node in got if abs(node["chainage_m"] - 351.0) < 1e-9)
|
||||
old = next(node for node in stored["base_pvi"] if abs(node["chainage_m"] - 351.0) < 1e-9)
|
||||
assert seated["elevation_m"] == pytest.approx(old["elevation_m"], abs=0.5)
|
||||
|
||||
|
||||
def test_세울_자리_없으면_Node_안_부름(calls) -> None:
|
||||
stored = DETAIL["longitudinal"]["profile_alignment"]
|
||||
assert confirm._seat_born_nodes(stored, stored["base_pvi"], [351.0]) == stored["base_pvi"]
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_추가_측점_따라감은_Node_입구(tmp_path, monkeypatch) -> None:
|
||||
from B05_Profile import B05_Profile_Plus_Stations as plus
|
||||
|
||||
seen: list[list[str]] = []
|
||||
|
||||
def spy(ids, inputs):
|
||||
seen.append(list(ids))
|
||||
return runner.run_calc(ids, inputs)
|
||||
|
||||
monkeypatch.setattr(plus, "run_calc", spy)
|
||||
old = [(0.0, 0.0), (100.0, 0.0)]
|
||||
plus.save_plus_stations(tmp_path, [30.0, 60.0], old)
|
||||
new = [(0.0, 5.0), (40.0, 5.0), (100.0, 25.0)] # 땅 위 자리를 새 노선에 투영
|
||||
moved = plus.follow_plus_stations(tmp_path, new)
|
||||
assert seen == [["facility.plus_follow"]]
|
||||
rows = plus.load_plus_stations(tmp_path)
|
||||
assert moved == 1 and [row["chainage_m"] for row in rows] == [30.0, 57.0] # 옛 파이썬 값
|
||||
assert [(row["x"], row["y"]) for row in rows] == [(30.0, 0.0), (60.0, 0.0)] # 좌표 그대로
|
||||
|
||||
|
||||
def test_구조물_따라감은_Node_입구(monkeypatch) -> None:
|
||||
from types import SimpleNamespace
|
||||
|
||||
from common_util import common_util_route_follow as follow
|
||||
|
||||
seen: list[list[str]] = []
|
||||
|
||||
def spy(ids, inputs):
|
||||
seen.append(list(ids))
|
||||
return runner.run_calc(ids, inputs)
|
||||
|
||||
monkeypatch.setattr(follow, "run_calc", spy)
|
||||
items = [
|
||||
SimpleNamespace(chainage_m=60.0, start_m=55.0, end_m=65.0, x=60.0, y=0.0, placement=None),
|
||||
SimpleNamespace(chainage_m=30.0, start_m=None, end_m=None, x=None, y=None, placement=None),
|
||||
]
|
||||
result = follow.follow_structures(items, [(0.0, 5.0), (40.0, 5.0), (100.0, 25.0)])
|
||||
assert seen == [["facility.follow"]]
|
||||
assert (items[0].chainage_m, items[0].start_m, items[0].end_m) == (57.0, 52.0, 62.0)
|
||||
assert items[1].chainage_m == 30.0 and result[1] == (None, False) # 좌표 없는 옛 자료 그대로
|
||||
assert result[0][0] == pytest.approx(11.068, abs=1e-3) and result[0][1] is False # 옛 파이썬 값
|
||||
Reference in New Issue
Block a user