fix(종단): 노선 손 고침 [저장] 바뀐 구간 계획고 = 옛 종단 길이 비례(지반 아님) · 손 [저장]이 「종단 반영」 안 덮음 (PLAN 575-2 · 요구 432 · 434 · 435)

- carry_base(파이썬) ↔ carryBase(TS) 짝 — 같은 자리 = 맞춘 옛 누가거리 · 바뀐 자리 = 앞뒤 같은 자리(없으면 시점 0 · 종점 새 연장 ↔ 옛 연장) 사이 새 누가거리 비례로 옛 누가거리 · 높이 = 옛 기준 꼭짓점 · 지반 첫 계획선은 측점 자리만
- 덧 흠 — 손 [저장](as_initial 아님)은 initial_choice with_profile 을 적힌 값 그대로
- 낡은 시험 고침: test_400 · test_344_2 「바뀐 자리 = 지반」 · 「끝 꼭짓점 = 새 지반」 → 옛 종단 비례 · 옛 끝 높이 / test_459 비균형 「옮긴 시설 측점 = 새 원지반」 단언 지움(옛 규칙 · 시설 세움은 옮긴 계획선 높이) · 거울 단언은 그대로
- 새 시험: test_400 바뀐 자리 비례 4 갈래 · test_346_3 손 [저장] 종단 반영 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 09:25:12 +09:00
co-authored by Claude Opus 5.5
parent 61373d70b0
commit f5dbb6e3dc
9 changed files with 125 additions and 53 deletions
+23 -12
View File
@@ -1,4 +1,5 @@
"""노선이 바뀔 때 계획고 옮김 — 같은 자리는 옛 계획고 · 바뀐 자리만 새 지반 첫 계획선(344-2 · 400).
"""노선이 바뀔 때 계획고 옮김 — 같은 자리는 옛 계획고 · 바뀐 자리는 옛 종단 길이 비례
(344-2 · 400 · 575 B).
사용자 확정(10-16 · 341 결정 1): 노선 끝쪽을 고쳤는데 앞쪽 계획고가 날아가지 않게 한다.
요구 250-2둘째(400): 평면을 고쳐도 크게 안 바뀐 자리의 종단 편집(직선화 · 자유지정 · 쉬프트 ·
@@ -9,7 +10,8 @@
- 같은 자리(`line_matcher`) = 새 선 점을 옛 선에 투영 · 옆 `SAME_LINE_TOL_M` 안 · 방향 30° 안
(측점 스냅이 뒤 교점을 밀어도 이어받음).
- 자동 꼭짓점(`carry_base`) · 편집(Node `carry_seat`)은 **새 측점 위에** 다시 놓음 — 같은 자리면
옛 선 그 자리 높이 · 아니면 새 지반 첫 계획선 · 편집은 옛 사용자 꼭짓점이 닿은 자리만 ·
옛 선 그 자리 높이 · 아니면 앞뒤 같은 자리 사이 길이 비례 자리 옛 높이 · 편집은 옛 사용자
꼭짓점이 닿은 자리만 ·
R 은 가까운 측점. 편집 셈 = TS `carryEdits`(서버 Node · 511-B B-3) — 파이썬 짝 없음.
짝 = TS `B05_Profile/logic/B05_Profile_Grade_Carry.ts`(같은 규칙) · 거울 시험 =
@@ -124,21 +126,30 @@ def carry_base(
ground_pvi: list[dict[str, float]],
tol_m: float = SAME_LINE_TOL_M,
) -> list[dict[str, float]]:
"""새 자동 꼭짓점 — 지반 첫 계획선 측점마다 같은 자리면 옛 자동선 높이 · 아니면 지반."""
"""새 자동 꼭짓점 — 지반 첫 계획선 측점 자리마다 옛 자동선 높이(575 B).
같은 자리 = 맞춘 옛 누가거리 · 바뀐 자리 = 앞뒤 같은 자리(없으면 노선 시점 · 종점) 사이를 새
누가거리 비례로 옛 누가거리에 옮김(옛 종단 모양을 늘이고 줄임 · 다시 풀지 않음).
"""
if not old_base or not ground_pvi:
return [dict(node) for node in old_base]
match = line_matcher(old_xy, new_xy, tol_m)
base_s = np.array([float(n["chainage_m"]) for n in old_base], dtype=np.float64)
base_z = np.array([float(n["elevation_m"]) for n in old_base], dtype=np.float64)
chainages = [float(node["chainage_m"]) for node in ground_pvi]
hits = [match(t) for t in chainages]
# 비례 기준점 — 시점(0 · 0) · 같은 자리 · 종점(새 · 옛 연장) 중 두 누가거리 모두 늘어나는 것만
# ponytail: 앞에서부터 고름 — 같은 방향 먼 옛 토막에 잘못 맞은 점이 있으면 뒤 기준점을 버림
at, to = [0.0], [0.0]
ends = (_cumulative(list(new_xy))[-1], _cumulative(list(old_xy))[-1])
for t, s in [*zip(chainages, hits), ends]:
if s is not None and t > at[-1] and s > to[-1]:
at.append(t)
to.append(s)
out = []
for node in ground_pvi:
s = match(float(node["chainage_m"]))
z = (
float(node["elevation_m"])
if s is None
else round(float(np.interp(s, base_s, base_z)), 6)
)
out.append({"chainage_m": float(node["chainage_m"]), "elevation_m": z})
for t, s in zip(chainages, hits):
s = float(np.interp(t, at, to)) if s is None else s
out.append({"chainage_m": t, "elevation_m": round(float(np.interp(s, base_s, base_z)), 6)})
return out
@@ -149,7 +160,7 @@ def carry_policy(
new_alignment: dict[str, Any],
) -> dict[str, Any]:
"""노선 [저장] 정책(432) — 새 정책에 옛 기본 L · 옛 정책 없으면 새 정책. 바뀐 자리 새 꼭짓점은
새 지반 첫 계획선(직선화 438 · `carry_base`)."""
새 지반 첫 계획선 측점 자리에 옛 높이 비례(`carry_base` · 575 B)."""
new_policy = dict(new_alignment.get("policy") or {})
old_policy = old_alignment.get("policy") or {}
if not old_policy:
+2 -2
View File
@@ -24,7 +24,7 @@ API — `POST /api/projects/{id}/route/replan`(`B05_Profile_Router_Replan.replan
- 손 고침 [저장](교점 · R · 스냅 · 간격 · 요구 289 · 290) = 보이는 화면 그대로 — 관 정본을
지우지 않음 · 좌표를 따라감 · 새 세류선 교차점 기본 관만 더함
(`B05_Profile_Route_Save_Parts`) · 배수유역 저장분은 노선 지문이 달라 「낡음」(B04 `_resolve`).
- 계획고 = 바뀐 자리만 새 지반 첫 계획선 · 같은 자리(옆 1 m 안) 사용자 계획고 지킴
- 계획고 = 바뀐 자리는 옛 종단 길이 비례(575 B) · 같은 자리(옆 1 m 안) 사용자 계획고 지킴
(`B05_Profile_Engine_Grade_Carry` · 옛 · 새 **계획 폴리라인** — 화면 planline 과 같은 선) ·
추천 [적용](`as_initial`) = 옮기지 않음 — 균형 종단(`apply_profile_target`) · 1차 계획선(438) ·
횡단 손 입력도 안 넘김(요구 277 · 측점마다 기본 설계).
@@ -254,7 +254,7 @@ async def _carry_profile(
old_xy: list[list[float]],
new_xy: list[list[float]],
) -> None:
"""같은 자리(옆 1 m 안) 옛 계획고 · R 을 새 측점 위에 · 바뀐 자리만 새 지반 첫 계획선(400)."""
"""같은 자리(옆 1 m 안) 옛 계획고 · R 을 새 측점 위에 · 바뀐 자리는 옛 종단 길이 비례(400 · 575 B)."""
from B05_Profile.B05_Profile_Engine_Grade_Alignment import AlignmentPolicy
from B05_Profile.B05_Profile_Engine_Grade_Carry import carry_base, carry_policy, changed_span
from B05_Profile.B05_Profile_Engine_Grade_Node import carry_seat
+4 -1
View File
@@ -588,7 +588,10 @@ async def _save_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, Any
choice.mode,
choice.weights.model_dump() if choice.weights else None,
(choice.criteria.model_dump(exclude_none=True) or None) if choice.criteria else None,
request.balanced_profile, # 「종단 반영」 — 다시 열면 되살림(420)
# 「종단 반영」 — 다시 열면 되살림(420) · 손 [저장]은 적힌 값 그대로(575)
request.balanced_profile
if as_initial
else read_initial_choice(project_root)["with_profile"],
)
if choice
else await asyncio.to_thread(read_initial_choice, project_root)
+25 -9
View File
@@ -1,6 +1,6 @@
/* =============================================================================
* B05_Profile/logic/B05_Profile_Grade_Carry.ts
* 노선이 바뀔 때 계획고 옮김 — 같은 자리는 옛 계획고 · 바뀐 자리만 새 지반 첫 계획선(344-2 · 400).
* 노선이 바뀔 때 계획고 옮김 — 같은 자리는 옛 계획고 · 바뀐 자리는 옛 종단을 길이 비례로(344-2 · 400 · 575 B).
*
* 짝 = 파이썬 `B05_Profile_Engine_Grade_Carry.py`(서버 정본 · 같은 규칙) · 거울 시험 =
* resources/tester/test_400_profile_carry.py · test_344_2_route_pairs.py.
@@ -8,7 +8,7 @@
* - 같은 자리(`lineMatcher`) = 새 선 점을 옛 선에 투영 · 옆 `SAME_LINE_TOL_M` 안 · 방향 30° 안
* (측점 스냅이 뒤 교점을 밀어도 이어받음).
* - 자동 꼭짓점(`carryBase`) · 편집(`carryEdits`)은 **새 측점 위에** 다시 놓음 — 같은 자리면 옛 선 그 자리
* 높이 · 아니면 새 지반 첫 계획선 · 편집은 옛 사용자 꼭짓점이 닿은 자리만 · R 은 가까운 측점.
* 높이 · 아니면 앞뒤 같은 자리 사이 길이 비례 자리 옛 높이 · 편집은 옛 사용자 꼭짓점이 닿은 자리만 · R 은 가까운 측점.
* ========================================================================== */
import {
@@ -159,7 +159,11 @@ export function lineMatcher(
};
}
/** 새 자동 꼭짓점 — 지반 첫 계획선 측점마다 같은 자리면 옛 자동선 높이 · 아니면 지반(파이썬 `carry_base` 짝). */
/**
* 새 자동 꼭짓점 — 지반 첫 계획선 측점 자리마다 옛 자동선 높이(575 B · 파이썬 `carry_base` 짝).
* 같은 자리 = 맞춘 옛 누가거리 · 바뀐 자리 = 앞뒤 같은 자리(없으면 노선 시점 · 종점) 사이를 새 누가거리 비례로
* 옛 누가거리에 옮김(옛 종단 모양을 늘이고 줄임 · 다시 풀지 않음).
*/
export function carryBase(
oldXy: ReadonlyArray<XY>,
newXy: ReadonlyArray<XY>,
@@ -171,13 +175,25 @@ export function carryBase(
const match = lineMatcher(oldXy, newXy, tolM);
const baseS = oldBase.map((node) => node.chainage_m);
const baseZ = oldBase.map((node) => node.elevation_m);
return groundPvi.map((node) => {
const s = match(node.chainage_m);
return {
chainage_m: node.chainage_m,
elevation_m: s === null ? node.elevation_m : roundTo(interp(baseS, baseZ, s), 6),
};
const hits = groundPvi.map((node) => match(node.chainage_m));
// 비례 기준점 — 시점(0 · 0) · 같은 자리 · 종점(새 연장 · 옛 연장) 중 두 누가거리 모두 늘어나는 것만
// ponytail: 앞에서부터 고름 — 같은 방향 먼 옛 토막에 잘못 맞은 점이 있으면 뒤 기준점을 버림
const at = [0];
const to = [0];
const pairs = groundPvi.map((node, index) => ({ t: node.chainage_m, s: hits[index] }));
const newCum = cumulative(newXy);
const oldCum = cumulative(oldXy);
pairs.push({ t: newCum[newCum.length - 1], s: oldCum[oldCum.length - 1] });
pairs.forEach(({ t, s }) => {
if (s !== null && t > at[at.length - 1] && s > to[to.length - 1]) {
at.push(t);
to.push(s);
}
});
return groundPvi.map((node, index) => ({
chainage_m: node.chainage_m,
elevation_m: roundTo(interp(baseS, baseZ, hits[index] ?? interp(at, to, node.chainage_m)), 6),
}));
}
/**
@@ -8,7 +8,8 @@
* 추가 측점 좌표 따라감(459 · `followPlusGround` · 좌표 들고 감 = 서버 [저장] 자리 · 493-3) ·
* 노선 바뀐 [저장]의 계획고 손댐(`touch` · 저장분 옮김과 다른 것만 · 498-2).
* planline — 노선 초안이 없으면 저장 종단 기준 그대로 · 있으면 새 측점마다 같은 자리는 저장 꼭짓점
* 높이 · 바뀐 자리만 새 지반 첫 계획선(`Grade_Ground` · `carryBase`) · 옮긴 시설 측점(바뀐 자리 ·
* 높이 · 바뀐 자리는 저장 꼭짓점 길이 비례(575 B · 측점 자리 = 새 지반 첫 계획선 `Grade_Ground` ·
* `carryBase`) · 옮긴 시설 측점(바뀐 자리 ·
* 사용자가 옮김)에 427 꼭짓점 다시 세움(459 · 짝 = 파이썬 `B05_Profile_Route_Save_Parts`
* `route_moved_stations` · 서버 세움 = Node `profile.carry` · 거울 시험 test_459_route_seat_mirror.py).
* profile — `buildAlignment`(첫 계획선 + 계획고 편집) — 종단 보기가 쓰는 같은 함수.
@@ -447,7 +448,8 @@ export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
})),
};
};
// 바뀐 자리 새 꼭짓점 = 서버 새 지반 첫 계획선(기본 L = config 짝 · 초기값 직선화 · 438) · 계산 자리
// 바뀐 자리 새 꼭짓점 자리 = 서버 새 지반 첫 계획선(기본 L = config 짝 · 초기값 직선화 · 438 · 높이는
// `carryBase` 가 옛 종단 비례 · 575 B) · 계산 자리
// 「서버」 면 서버 Node(`groundPvi` · 511-B B-3)
const input = {
samples,