fix(459 노선 바뀜): [저장] 구조물 초안을 서버 따라감 위에 · 추가 측점 초안 동안 좌표 따라감 · 옮긴 시설 측점 427 꼭짓점 세움 · 관 따라감 선 = points

- 구조물: 노선 저장 뒤 rebaseStructureDraft(관 rebasePipeDraft 짝) — 따라가기만 한 것 = 서버 자리 · 사용자가 옮긴 것 = 초안
- 추가 측점: follow 단계가 좌표로 옮긴 정수 m 를 patch.plusStations 로(FOLLOW_KEYS · stations 단계 입력값) · followPlusDraft(가게 쓰기 · 초안 버림 되돌림 · page 부름은 sub7) · follow 만 쓴 초안은 [저장]이 PUT 안 함
- 427: 초안 planline · 서버 _carry_profile 이 바뀐 구간 관 · 구조물 측점(+ 초안 중 사용자가 옮긴 것)에 seatBornNodes 로 다시 세움(route_moved_stations · seat_route_moved 짝)
- 서버 관 따라감 = 구조물과 같은 points(사용 범위 자름) · 좌표 없는 옛 관은 옛 노선 자리로 먼저 · 따라감 실패 = follow_failed 알림
- 시험 test_459_route_seat_mirror(거울 · 임시 storage · _place_pipes) · 헬퍼 400 · 449 · design_store_save 가짜 창구

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 22:50:43 +09:00
co-authored by Claude Opus 5.5
parent b4ae9d10b5
commit 22bf1e9822
16 changed files with 743 additions and 19 deletions
+62
View File
@@ -13,6 +13,8 @@
* 아니면 누가거리의 노선 위 자리 · 410) · 거울 시험 = resources/tester/test_410_follow_xy.py.
* - `placeOnLine` 의 좌표 없는 관 · `rebasePipeDraft` 는 TS 만 — 좌표 없는 세션 관 초안(정본 PUT 꼴)을 따라가기
* 전 자리로 · 노선을 저장한 [저장]이 서버가 따라 옮긴 관 위에 초안을 얹음(349-0u).
* - `rebaseStructureDraft` 도 TS 만 — 구조물 초안을 서버가 따라 옮긴 정본 위에(459 · 관 짝).
* - `followPlusStations` ↔ 파이썬 `B05_Profile_Plus_Stations.follow_plus_stations`(459 · 노선 초안 동안).
* 거울 시험 = resources/tester/test_344_2_route_pairs.py.
* ========================================================================== */
@@ -142,6 +144,66 @@ export function rebasePipeDraft<P extends FollowingPoint & { id?: string | null
return [...kept, ...added].sort((a, b) => a.chainage_m - b.chainage_m);
}
type StructureLike = FollowingStructure & { structure_id?: string | null };
/** 자리 셋(기준 · 구간 끝)이 같은가 — `shift` 만큼 민 자리로 견줌(구간 길이 = 시설 치수). */
const samePositions = (a: FollowingStructure, b: FollowingStructure, shift = 0): boolean =>
(["chainage_m", "start_m", "end_m"] as const).every(
(key) =>
(a[key] == null) === (b[key] == null) &&
Math.abs((a[key] ?? 0) + shift - (b[key] ?? 0)) <= SAME_STATION_M,
);
/** 노선을 저장한 [저장] — 세션 구조물 초안을 서버가 따라 옮긴 정본(`after`) 위에 얹은 목록(459 · 관
* `rebasePipeDraft` 짝). 따라가기만 한 것(자리 = 노선 저장 전 정본 `before` 그대로 · 또는 땅 위 자리가 같고
* 기준 누가거리가 서버 따라감과 같음)은 서버 자리(기준 · 구간 끝 · 좌표) · 사용자가 옮긴 것(마커 · 폼 =
* 누가거리만 옮겨 좌표 낡음 포함) · 새로 놓은 것은 초안 그대로 · 옵션 · 메모는 늘 초안 · 짝은 `structure_id`. */
export function rebaseStructureDraft<S extends StructureLike>(
draft: ReadonlyArray<S>,
before: ReadonlyArray<StructureLike>,
after: ReadonlyArray<StructureLike>,
): S[] {
const old = new Map(before.map((item) => [item.structure_id, item]));
const moved = new Map(after.map((item) => [item.structure_id, item]));
return draft.map((item) => {
const was = item.structure_id ? old.get(item.structure_id) : undefined;
const now = item.structure_id ? moved.get(item.structure_id) : undefined;
if (!was || !now) return item;
const followed =
item.x != null &&
item.y != null &&
was.x != null &&
was.y != null &&
Math.hypot(item.x - was.x, item.y - was.y) <= FOLLOW_SAME_XY_M &&
samePositions(was, item, (item.chainage_m ?? 0) - (was.chainage_m ?? 0)) &&
Math.abs((item.chainage_m ?? 0) - (now.chainage_m ?? 0)) <= SAME_STATION_M;
if (!followed && !samePositions(item, was)) return item;
return {
...item,
chainage_m: now.chainage_m ?? item.chainage_m,
start_m: now.start_m ?? null,
end_m: now.end_m ?? null,
x: now.x ?? item.x,
y: now.y ?? item.y,
};
});
}
/** 추가 측점이 노선을 따라감(459) — 옛 선(`from`) 위 누가거리의 땅 위 자리를 새 선(`to`)에 투영한 정수 m
* (같은 m 하나 · 오름차순). 파이썬 `follow_plus_stations` 짝(`follow_structures` 같은 셈). */
export function followPlusStations(
list: ReadonlyArray<number>,
from: ReadonlyArray<XY>,
to: ReadonlyArray<XY>,
): number[] {
const placed = placeOnLine(
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);
}
/** 따라가기만 한 관 = 정본과 같은 땅 위 자리(m) — 서버 좌표가 mm 3자리라 그 반올림 폭. */
const FOLLOW_SAME_XY_M = 0.001;