fix(475-1 노선 자리): 노선 선 소수 자리 한 벌 — 서버 노선 geojson x · y 4자리(= 계획노선 csv) · 브라우저 초안 그린 선 · 곡선표도 같은 자리
- B05_Profile_Engine._route_geojson x · y 3 → 4자리(z 3자리 그대로) · 짝 머리 표시 - draftLine 그린 선 x · y 4자리 · 곡선 start · apex · end 4자리 · R 3자리(= RouteCurve.as_dict) · 파이썬 round 짝(toFixed) - 교점 끌기 재현 브라우저 ↔ 서버 차 0.81 → 0.00 ㎥ · 옹벽 + 추가 측점 0.88 → 0.00 ㎥(test_475_1_route_digits) - 472-1 헬퍼 — 옮김 인자 · 페이지 배선(stationsAt · storedPvi) · 단면 모양 = 따라간 구조물(헛 차 덤 고침) · golden 값 그대로 - 옛 시험 두 곳(346_1) — 안 깎은 선 단언 → 서버 저장 자리 단언 - 관 지문 허용오차 주석 · calculations R1 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,12 +22,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _route_geojson(polyline: list[list[float]]) -> dict[str, Any]:
|
||||
"""폴리라인을 3D LineString GeoJSON Feature로 변환한다."""
|
||||
"""폴리라인을 3D LineString GeoJSON Feature로 변환한다.
|
||||
|
||||
x · y 4자리 = 계획노선 csv(`write_route_csv`) · 브라우저 route 단계(`draftLine` · 짝 —
|
||||
`test_475_1_route_digits.py`) — 같은 선이라야 저장 체인 ↔ 화면 누가토량이 같음(475-1).
|
||||
"""
|
||||
return {
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [[round(x, 3), round(y, 3), round(z, 3)] for x, y, z in polyline],
|
||||
"coordinates": [[round(x, 4), round(y, 4), round(z, 3)] for x, y, z in polyline],
|
||||
},
|
||||
"properties": {},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* 실시간 계산 묶음(PLAN 346-1)의 **route · ground 단계** — 계약 = `design_pipeline_types.ts`.
|
||||
*
|
||||
* route — 노선 초안(RouteDraft) → 그린 노선(원호 포함) · 누가거리. 선은 이미 있는 짝
|
||||
* `buildEditedPolyline`(↔ `common_util_route_polyline.py`) 그대로.
|
||||
* `buildEditedPolyline`(↔ `common_util_route_polyline.py`) 그대로 · 자릿수 = 서버 저장 선
|
||||
* (x · y 4자리 ↔ `_route_geojson` · `write_route_csv` · 475-1 · `test_475_1_route_digits.py`).
|
||||
* ground — 측점(지반 null) → 지반 채운 측점 · 종단 지반선(1 m). 표본 = 지형 격자 짝
|
||||
* (`common_util_surface_sampler.ts` · `B05_Profile_Sections_Ground.ts`) · 격자가 없거나
|
||||
* (아직 못 받음 · DTM 아님) 분석 지형 ≠ 설계 지형이면 주입받은 서버 표본(`remote`).
|
||||
@@ -54,8 +55,14 @@ export function routeLineOf(line: Vertex[], curves: DesignCurve[] = []): RouteLi
|
||||
return { line, curves, chainage, lengthM: chainage[chainage.length - 1] };
|
||||
}
|
||||
|
||||
/** 파이썬 `round(v, n)` 짝 — 둘 다 double 의 정확한 값으로 반올림(4 · 3자리 동점 없음). */
|
||||
const roundTo = (value: number, digits: number): number => Number(value.toFixed(digits));
|
||||
const xy4 = ([x, y]: Vertex): Vertex => [roundTo(x, 4), roundTo(y, 4)];
|
||||
|
||||
/** 초안 → 그린 노선 · 곡선표(계약 꼴). 길이를 붙든 곡선은 교각에 맞춰 R 을 다시 잡음(편집기
|
||||
* `markEdited` 와 같은 차례). */
|
||||
* `markEdited` 와 같은 차례). 자릿수 = 서버가 저장하는 선(475-1 · 짝 = 계획노선 csv
|
||||
* `write_route_csv` · 곡선표 `RouteCurve.as_dict` · 노선 geojson `_route_geojson` x · y 4자리 · R 3자리 ·
|
||||
* `test_475_1_route_digits.py`). */
|
||||
export function draftLine(
|
||||
draft: RouteDraft,
|
||||
minRadiusM: number,
|
||||
@@ -65,8 +72,13 @@ export function draftLine(
|
||||
applyArcLocks(nodes, [...draft.curveLock], [...draft.curveArc], radius);
|
||||
const built = buildEditedPolyline(nodes, draft.curveOn, radius, minRadiusM);
|
||||
return {
|
||||
line: built.vertices,
|
||||
curves: built.curves.map(({ start, apex, end, radius_m }) => ({ start, apex, end, radius_m })),
|
||||
line: built.vertices.map(xy4),
|
||||
curves: built.curves.map(({ start, apex, end, radius_m }) => ({
|
||||
start: xy4(start),
|
||||
apex: xy4(apex),
|
||||
end: xy4(end),
|
||||
radius_m: roundTo(radius_m, 3),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@ def fill_pipe_coordinates(points: list[PipePoint], vertices: list[RouteVertex])
|
||||
# `planned_route.csv`(소수 4자리)와 `route_main.geojson`(소수 3자리, csv 를 mm 로 반올림한
|
||||
# 사본)로 **0.5mm 다르게** 저장돼 있어, 그 0.5mm 가 `.xx5` 경계를 넘는 정점마다 글자가
|
||||
# 바뀐다(2026-09-07 실측: 169개 중 **16개**). 노선을 손댄 적이 없는데도 지문이 늘 달랐다.
|
||||
# 475-1 부터 geojson 도 4자리(새로 쓴 노선) — 옛 3자리 저장분이 남으므로 허용오차는 그대로.
|
||||
#
|
||||
# 경계에서 자르는 방식은 저장 자릿수가 또 바뀌면 다시 흔들리므로 **글자 일치 대신
|
||||
# 허용오차**로 가른다. 값은 0.05m — 위 어긋남이 관 누가거리에 미치는 양이 실측
|
||||
|
||||
@@ -14,7 +14,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
|
||||
|
||||
| # | 계산 | TS | 파이썬 짝 | Node | 실행 자리 · 거울 시험 |
|
||||
|---|---|---|---|---|---|
|
||||
| R1 | 평면 폴리라인 · 곡선(BC · EC · MC) | `logic/B05_Profile_UI_RouteEdit_Curve.ts:243` · `logic/B05_Profile_Pipeline_RouteGround.ts:73` | `common_util_route_polyline.py:579` | — | 짝 · `test_route_polyline_browser_mirror` |
|
||||
| R1 | 평면 폴리라인 · 곡선(BC · EC · MC) · 선 자리 x · y 4자리 한 벌(초안 그린 선 = 계획노선 csv = 노선 geojson) | `logic/B05_Profile_UI_RouteEdit_Curve.ts:243` · `logic/B05_Profile_Pipeline_RouteGround.ts:66` draftLine | `common_util_route_polyline.py:579` · `common_util_route_geometry.py` write_route_csv · `B05_Profile_Engine.py:24` _route_geojson | — | 짝 · `test_route_polyline_browser_mirror` · `test_475_1_route_digits` |
|
||||
| R2 | 손잡이 → IP · R | `logic/B05_Profile_UI_RouteEdit_Curve.ts` dragHandleTo | — | — | 브라우저 · `test_route_polyline_handle_drag` |
|
||||
| R3 | 노드 편집 규칙(곡선 한도 · 고정점) | `logic/B05_Profile_UI_RouteEdit_Edits.ts` | `B05_Profile_Route_NodeEdits.py:21` | — | 짝 · `test_430_route_edit_rules` |
|
||||
| R4 | 스냅 맞춤(MC → 일반측점) | `logic/B05_Profile_RouteEdit_StationFit.ts` | — | — | 브라우저 · `test_366_3_station_fit` |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* 472-1 헬퍼 — 브라우저 길 기준값(진짜 단계 함수 · vite ssr 묶음 · helper_472_1_browser_entry.ts).
|
||||
* node helper_472_1_browser.mjs <묶음.mjs> <고정 자료 폴더> <격자 .bin>
|
||||
* node helper_472_1_browser.mjs <묶음.mjs> <고정 자료 폴더> <격자 .bin> [옮김 .json]
|
||||
* 옮김(475-1) = { node, dx, dy, structures?, plus? } — 열기 뒤 `pipe.update("route", 초안)`(노드 하나 옮김 · 페이지
|
||||
* `flattenServerPlan` 초안 꼴) · 구조물 · 추가 측점 바꿔 넣기.
|
||||
* 페이지 묶음(`design_pipeline_page`)과 같은 단계 붙임 — 저장 노선 · 초안 없음 · 세션 = 메모리 가짜 · fetch 없음 ·
|
||||
* 격자 = 서버 `dtm_grid_bytes` 가 고정 npz 로 지은 이진. 묶음 출력 → 새 노선 상세(`draftSectionDetail`) → 구조물
|
||||
* 면적 얹기 → 누가토량. 표준출력 JSON 한 벌(값 꼴 = test_472_1_golden.py `values_of`). */
|
||||
@@ -32,16 +34,17 @@ globalThis.document = any;
|
||||
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||
globalThis.fetch = () => Promise.reject(new Error("서버 없음"));
|
||||
|
||||
const [bundle, dir, gridPath] = process.argv.slice(2);
|
||||
const [bundle, dir, gridPath, movePath] = process.argv.slice(2);
|
||||
const move = movePath ? JSON.parse(readFileSync(movePath, "utf8")) : {};
|
||||
const m = await import(pathToFileURL(bundle).href);
|
||||
const read = (name) => JSON.parse(readFileSync(join(dir, name), "utf8"));
|
||||
const detail = read("detail.json");
|
||||
const plan = read("plan.json");
|
||||
const context = read("context.json");
|
||||
const pipes = read("pipes.json").pipe_points.map(({ lonlat: _l, ...pipe }) => pipe);
|
||||
const structures = read("structures.json").structures;
|
||||
const structures = move.structures ?? read("structures.json").structures;
|
||||
const types = read("structure_types.json").types;
|
||||
const plus = read("plus_stations.json").chainages;
|
||||
const plus = move.plus ?? read("plus_stations.json").chainages;
|
||||
const P = context.project_id;
|
||||
const R = context.route_id;
|
||||
const bin = readFileSync(gridPath);
|
||||
@@ -66,7 +69,6 @@ const savedStations = m.buildStations(savedLine(), savedCurves(), pipes, structu
|
||||
});
|
||||
const crossSource = m.pipelineCrossSource(P, detail, savedStations, R);
|
||||
const edits = m.readAlignment(detail.longitudinal).edits;
|
||||
const status = m.shapeStatus(pipes, structures, types);
|
||||
const bermAt = (list) => {
|
||||
const spans = m.bermSpansFromStructures(Array.isArray(list) ? list : []);
|
||||
return (chainageM) => m.bermSpecAt(spans, chainageM);
|
||||
@@ -81,6 +83,17 @@ const pipe = m.createDesignPipeline({
|
||||
follow: m.createFollowStage({
|
||||
savedLine,
|
||||
saved: () => ({ pipes, structures, alignment: edits }),
|
||||
// 페이지 배선(`design_pipeline_page` stationsAt)
|
||||
stationsAt: (route, patch, inputs) =>
|
||||
m
|
||||
.buildStations(
|
||||
route.line,
|
||||
route.curves,
|
||||
patch.pipes ?? inputs.pipes ?? [],
|
||||
patch.structures ?? inputs.structures ?? [],
|
||||
{ intervalM: interval, types: enabled, ...m.snapOptions(inputs.route, plan) },
|
||||
)
|
||||
.map((station) => station.chainageM),
|
||||
}),
|
||||
stations: m.createStationsStage({
|
||||
intervalM: interval,
|
||||
@@ -94,6 +107,7 @@ const pipe = m.createDesignPipeline({
|
||||
planline: m.createPlanlineStage({
|
||||
savedBase: (at) => m.plantedBase({ base: null, detail }, at, types),
|
||||
savedLine,
|
||||
storedPvi: () => m.toAlignmentBase(m.readAlignment(detail.longitudinal)).basePvi,
|
||||
}),
|
||||
profile: m.profileStage,
|
||||
cross: (ctx) =>
|
||||
@@ -122,6 +136,27 @@ const record = await pipe.start({
|
||||
});
|
||||
await pipe.settled();
|
||||
if (record.outcome !== "done") throw new Error(`묶음 ${record.outcome}`);
|
||||
if (move.node != null) {
|
||||
const flat = m.flattenServerPlan(plan.nodes, plan.curves ?? [], plan.node_edits);
|
||||
const nodes = flat.planned.map(([x, y]) => [x, y]);
|
||||
nodes[move.node] = [nodes[move.node][0] + move.dx, nodes[move.node][1] + move.dy];
|
||||
const moved = await pipe.update("route", {
|
||||
rev: 1,
|
||||
nodes,
|
||||
curveOn: flat.curveOn.map((on) => on !== false),
|
||||
curveRadius: flat.curveRadius.map((r) => r ?? null),
|
||||
curveLock: flat.curveLock ?? nodes.map(() => null),
|
||||
curveArc: flat.curveArc ?? nodes.map(() => null),
|
||||
stationSnap: plan.station_snap ?? false,
|
||||
stationSnapStep: plan.station_snap_step ?? null,
|
||||
terrain: null,
|
||||
});
|
||||
await pipe.settled();
|
||||
if (moved.outcome !== "done") throw new Error(`묶음(옮김) ${moved.outcome}`);
|
||||
}
|
||||
// 단면 모양 = 따라간 관 · 구조물(페이지 = follow 뒤 가게 값 · 457)
|
||||
const follow = pipe.output("follow");
|
||||
const status = m.shapeStatus(follow?.pipes ?? pipes, follow?.structures ?? structures, types);
|
||||
|
||||
const ground = pipe.output("ground");
|
||||
const profile = pipe.output("profile");
|
||||
@@ -149,6 +184,8 @@ process.stdout.write(
|
||||
violations: profile.violations ?? [],
|
||||
footprint_records: pipe.output("footprint")?.records?.length ?? 0,
|
||||
skipped: record.skipped,
|
||||
route_line: pipe.output("route").line,
|
||||
draft: pipe.input("route"),
|
||||
}),
|
||||
);
|
||||
process.exit(0);
|
||||
|
||||
@@ -34,3 +34,6 @@ export { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Secti
|
||||
export { parseDtmGrid } from "../../common_util/common_util_surface_sampler";
|
||||
export { computeMassHaul } from "../../common_util/common_util_mass_haul";
|
||||
export { writeState, writeStateRaw } from "../../A00_Common/b_page_state";
|
||||
export { flattenServerPlan } from "../../B05_Profile/logic/B05_Profile_UI_RouteEdit_Edits";
|
||||
export { snapOptions } from "../../A00_Common/design/logic/design_pipeline_stations";
|
||||
export { toAlignmentBase } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
|
||||
@@ -273,8 +273,10 @@ def stored(root: Path) -> dict[str, Any]:
|
||||
# ── 브라우저 길 ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def browser(work: Path) -> dict[str, Any]:
|
||||
"""vite ssr 로 진짜 모듈을 묶어 헬퍼를 돌림(`helper_272_detail_memory` 꼴) · 격자 = 고정 npz."""
|
||||
def browser(work: Path, move: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""vite ssr 로 진짜 모듈을 묶어 헬퍼를 돌림(`helper_272_detail_memory` 꼴) · 격자 = 고정 npz.
|
||||
|
||||
`move`(475-1) = 노드 하나 옮김 · 구조물 · 추가 측점 — 값에 그린 선 · 초안을 덧붙임."""
|
||||
from common_util.common_util_surface_sampler import dtm_grid_bytes
|
||||
|
||||
grid = work / "grid.bin"
|
||||
@@ -303,8 +305,12 @@ def browser(work: Path) -> dict[str, Any]:
|
||||
raise RuntimeError(build.stderr)
|
||||
bundle = work / "b" / "bundle.mjs" # 조각(assets/)과 같은 폴더
|
||||
shutil.copy(work / "b" / "helper_472_1_browser_entry.js", bundle)
|
||||
args = [str(HERE / "helper_472_1_browser.mjs"), str(bundle), str(FIXTURE), str(grid)]
|
||||
if move is not None:
|
||||
(work / "move.json").write_text(json.dumps(move), encoding="utf-8")
|
||||
args.append(str(work / "move.json"))
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_472_1_browser.mjs"), str(bundle), str(FIXTURE), str(grid)],
|
||||
["node", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
@@ -313,9 +319,13 @@ def browser(work: Path) -> dict[str, Any]:
|
||||
if proc.returncode:
|
||||
raise RuntimeError(proc.stderr)
|
||||
out = json.loads(proc.stdout)
|
||||
return values_of(
|
||||
values = values_of(
|
||||
out["sections"], out["keys"], out["mass_haul"], out["warnings"], out["violations"]
|
||||
)
|
||||
if move is not None:
|
||||
values["route_line"] = out["route_line"]
|
||||
values["draft"] = out["draft"]
|
||||
return values
|
||||
|
||||
|
||||
PATHS = ("stored", "server_save", "server_route", "browser")
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"""475-1 헬퍼 — 노선 저장 체인(DB 없이) · 노드 하나 옮긴 초안 → 서버 정본 값.
|
||||
|
||||
`helper_472_1_paths.server_route` 꼴에 노선 바뀜 갈래를 더함 —
|
||||
`_write_planned_polyline`(csv 4자리 · 곡선표) → 노선 geojson(`_route_geojson`) → 관 따라감 ·
|
||||
구조물 따라감 · 추가 측점 → `run_section_generation` → `_carry_profile`(옛 · 새 계획 폴리라인 ·
|
||||
`carry_base` · `seat_route_moved`) → `_carry_cross` →
|
||||
상세 읽기 → `_save_tail`. 짝 = `helper_472_1_browser.mjs` 옮김 인자 · `test_475_1_route_digits.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import helper_472_1_paths as P
|
||||
|
||||
|
||||
def server_moved(root: Path, move: dict[str, Any]) -> dict[str, Any]:
|
||||
from B05_Profile.B05_Profile_Engine import _route_geojson
|
||||
from B05_Profile.B05_Profile_Engine_AsPlanned import _vertex_z
|
||||
from B05_Profile.B05_Profile_Engine_Grade import GradeDesignOptions
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Carry import (
|
||||
carry_base,
|
||||
carry_edits,
|
||||
carry_policy,
|
||||
changed_span,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Sections import run_section_generation
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions
|
||||
from B05_Profile.B05_Profile_Plus_Stations import save_plus_stations
|
||||
from B05_Profile.B05_Profile_Route_Save_Parts import (
|
||||
carry_cross_inputs,
|
||||
follow_pipes,
|
||||
route_moved_stations,
|
||||
seat_route_moved,
|
||||
station_keys,
|
||||
vertices_of,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Router import _apply_alignment_edits
|
||||
from B05_Profile.B05_Profile_Router_Replan import _vertices_of, _write_planned_polyline
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B05_Profile.B05_Profile_Structures_Router import follow_structures_to_route
|
||||
from B06_Section.B06_Section_Engine_Design import ROUTE_CARRY_KEYS
|
||||
from B06_Section.B06_Section_Router import _read_section_detail
|
||||
from B06_Section.B06_Section_Router_Confirm import _rowless_station_chainages
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
CARRY_INPUT_KEYS,
|
||||
STRUCTURE_ADJUST_KEYS,
|
||||
attach_default_designs,
|
||||
compute_default_designs,
|
||||
)
|
||||
from common_util.common_util_drainage_pipes import (
|
||||
pipe_points_path_in,
|
||||
read_pipe_points_file,
|
||||
route_signature,
|
||||
)
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_route_geometry import (
|
||||
planned_route_initial_path,
|
||||
planned_route_working_path,
|
||||
)
|
||||
|
||||
saved = P.read("detail.json")
|
||||
summary = P.read("summary.json")["longitudinal"]["data"]
|
||||
options = {k: v for k, v in summary["options"].items() if k != "extra_stations"}
|
||||
old_line = _vertices_of(planned_route_working_path(root)) or _vertices_of(
|
||||
planned_route_initial_path(root)
|
||||
)
|
||||
old_geo = [
|
||||
[c[0], c[1]]
|
||||
for c in json.loads((root / P.ROUTE).read_text(encoding="utf-8"))["geometry"]["coordinates"]
|
||||
]
|
||||
old_pipes = read_pipe_points_file(pipe_points_path_in(root))
|
||||
if move.get("structures"):
|
||||
atomic_write_json(
|
||||
root / "B05_Profile" / "route" / "structures.json",
|
||||
{"revision": 1, "structures": move["structures"], "definition_version": None},
|
||||
)
|
||||
old_structures = load_structures(str(root))[1]
|
||||
# 노선 저장 = 초안 노드 그대로(브라우저 `flattenServerPlan` 초안 · 헬퍼 출력 draft_nodes)
|
||||
draft = move["draft"]
|
||||
working = planned_route_working_path(root)
|
||||
_write_planned_polyline(
|
||||
working,
|
||||
[tuple(n) for n in draft["nodes"]],
|
||||
12.0,
|
||||
simplify=False,
|
||||
curve_flags=list(draft["curveOn"]),
|
||||
radii=list(draft["curveRadius"]),
|
||||
station_snap=draft["stationSnap"],
|
||||
station_snap_step=draft["stationSnapStep"],
|
||||
locks=list(draft["curveLock"]),
|
||||
arcs=list(draft["curveArc"]),
|
||||
)
|
||||
points = _vertices_of(working)
|
||||
if move.get("structures"):
|
||||
follow_structures_to_route(root, P.ROUTE, [{"x": x, "y": y} for x, y in points])
|
||||
z = _vertex_z(root, "classification", "dtm", True, [tuple(p) for p in points])
|
||||
atomic_write_json(root / P.ROUTE, _route_geojson([[x, y, h] for (x, y), h in zip(points, z)]))
|
||||
verts = vertices_of(points)
|
||||
pairs = follow_pipes(old_pipes, verts)
|
||||
doc = json.loads(pipe_points_path_in(root).read_text(encoding="utf-8"))
|
||||
doc["route_signature"] = route_signature(verts)
|
||||
doc["points"] = [p.as_dict() for _, p in pairs]
|
||||
atomic_write_json(pipe_points_path_in(root), doc)
|
||||
if move.get("plus"):
|
||||
save_plus_stations(root, move["plus"], points)
|
||||
run_section_generation(
|
||||
root,
|
||||
P.ROUTE,
|
||||
*P.SURFACE,
|
||||
options=SectionGenerationOptions(**options),
|
||||
grade_options=GradeDesignOptions(**summary["grade_options"]),
|
||||
crs=f"EPSG:{P.read('context.json')['crs_epsg']}",
|
||||
)
|
||||
# _carry_profile — 옛 · 새 계획 폴리라인
|
||||
old_alignment = saved["longitudinal"]["profile_alignment"]
|
||||
longitudinal = json.loads((root / P.LONG).read_text(encoding="utf-8"))
|
||||
alignment = longitudinal.get("profile_alignment") or {}
|
||||
span = changed_span([tuple(p) for p in old_line], [tuple(p) for p in points])
|
||||
if span is None:
|
||||
base, edits = old_alignment["base_pvi"], old_alignment.get("edits") or {}
|
||||
else:
|
||||
stations = [float(r["chainage_m"]) for r in longitudinal.get("stations") or []]
|
||||
half = float((alignment.get("policy") or {}).get("station_interval_m") or 20.0) / 2
|
||||
base = carry_base(old_line, points, old_alignment["base_pvi"], alignment["base_pvi"])
|
||||
edits = carry_edits(
|
||||
old_line, points, old_alignment["base_pvi"], old_alignment.get("edits"), stations, half
|
||||
)
|
||||
alignment["policy"] = carry_policy(old_line, points, old_alignment, alignment)
|
||||
alignment["base_pvi"] = base
|
||||
if span is not None:
|
||||
born = route_moved_stations(longitudinal.get("stations") or [], old_line, points)
|
||||
held = [float(k) for k in (edits.get("station_offsets") or {})]
|
||||
alignment["base_pvi"] = seat_route_moved(alignment, born, held)
|
||||
longitudinal["profile_alignment"] = alignment
|
||||
atomic_write_json(root / P.LONG, longitudinal)
|
||||
alignment, _s, _d = _apply_alignment_edits(root, P.LONG, edits)
|
||||
# _carry_cross
|
||||
new_long = json.loads((root / P.LONG).read_text(encoding="utf-8"))
|
||||
_, structures = load_structures(str(root))
|
||||
interval = float(options["station_interval_m"])
|
||||
old_keys = station_keys(
|
||||
saved["longitudinal"]["stations"],
|
||||
[p for _, p in follow_pipes(old_pipes, vertices_of(old_geo))],
|
||||
old_structures,
|
||||
interval,
|
||||
)
|
||||
new_keys = station_keys(
|
||||
new_long.get("stations") or [], [p for _, p in pairs], structures, interval
|
||||
)
|
||||
designs = {
|
||||
round(float(s["chainage_m"]), 3): s["design"]
|
||||
for s in saved["cross_sections"]
|
||||
if isinstance(s.get("design"), dict)
|
||||
}
|
||||
inputs, adjusts, _dropped = carry_cross_inputs(
|
||||
old_keys, new_keys, designs, (*CARRY_INPUT_KEYS, *ROUTE_CARRY_KEYS), STRUCTURE_ADJUST_KEYS
|
||||
)
|
||||
chainages = _rowless_station_chainages(root, P.LONG, [])
|
||||
entries = compute_default_designs(root, P.LONG, chainages, None, inputs)
|
||||
by_m = {round(at, 3): {**d, **adjusts.get(round(at, 3), {})} for at, d in entries}
|
||||
detail = _read_section_detail(root, P.LONG, P.ROUTE)
|
||||
for section in detail["cross_sections"]:
|
||||
hit = next(
|
||||
(d for at, d in by_m.items() if abs(at - float(section["chainage_m"])) < 0.01), None
|
||||
)
|
||||
if hit is not None:
|
||||
section["design"] = hit
|
||||
attach_default_designs(detail["longitudinal"], detail["cross_sections"], root, None)
|
||||
keys = P._keys(detail["longitudinal"], detail["cross_sections"], root)
|
||||
sections, haul = P._save_tail(detail, root)
|
||||
out = P.values_of(
|
||||
sections, keys, haul, alignment.get("warnings") or [], alignment.get("violations") or []
|
||||
)
|
||||
out["route_line"] = [list(p) for p in points]
|
||||
return out
|
||||
@@ -66,7 +66,8 @@ def _line(nodes: list[list[float]]) -> list[list[float]]:
|
||||
curve_flags=[True] * len(nodes),
|
||||
radii=[None] * len(nodes),
|
||||
)
|
||||
return [list(v) for v in built.vertices]
|
||||
# 저장 노선 = 계획노선 csv 4자리(`write_route_csv` · 475-1 — 초안 그린 선도 같은 자리)
|
||||
return [[round(x, 4), round(y, 4)] for x, y in built.vertices]
|
||||
|
||||
|
||||
def _fake(folder: Path) -> tuple[Path, list[list[float]]]:
|
||||
|
||||
@@ -232,24 +232,16 @@ def test_route_ground_단계가_서버와_같고_범위_밖을_재사용한다(t
|
||||
radii=[None] * len(nodes),
|
||||
)
|
||||
route = out["route"]
|
||||
assert len(route["line"]) == len(built.vertices)
|
||||
assert (
|
||||
max(
|
||||
abs(a - b)
|
||||
for p, q in zip(route["line"], built.vertices, strict=True)
|
||||
for a, b in zip(p, q)
|
||||
)
|
||||
< 1e-9
|
||||
)
|
||||
steps = np.hypot(*np.diff(np.asarray(built.vertices), axis=0).T)
|
||||
# 그린 선 = 서버 저장 선 자리(x · y 4자리 · `write_route_csv` 짝 · 475-1)
|
||||
assert route["line"] == [[round(x, 4), round(y, 4)] for x, y in built.vertices]
|
||||
steps = np.hypot(*np.diff(np.asarray(route["line"]), axis=0).T)
|
||||
assert abs(route["lengthM"] - float(steps.sum())) < 1e-6
|
||||
assert route["chainage"][0] == 0 and len(route["chainage"]) == len(route["line"])
|
||||
assert len(route["curves"]) == len(built.curves) > 0
|
||||
for ts, py in zip(route["curves"], built.curves, strict=True):
|
||||
assert set(ts) == {"start", "apex", "end", "radius_m"}
|
||||
assert abs(ts["radius_m"] - py.radius_m) < 1e-9
|
||||
for key in ("start", "apex", "end"):
|
||||
assert max(abs(a - b) for a, b in zip(ts[key], getattr(py, key))) < 1e-9
|
||||
# 곡선표 = 서버 저장 곡선표 자리(`RouteCurve.as_dict` · 475-1)
|
||||
assert ts == {k: py.as_dict()[k] for k in ts}
|
||||
assert out["saved"]["line"] == line
|
||||
assert out["saved"]["curves"] == []
|
||||
assert out["noRoute"]
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""475-1 — 노선 선 소수 자리 한 벌(요구 275 ① · 브라우저 ↔ 서버 저장 차).
|
||||
|
||||
짝 = 서버 노선 geojson `_route_geojson` x · y 4자리 = 계획노선 csv `write_route_csv` =
|
||||
브라우저 route 단계 `draftLine`(`B05_Profile_Pipeline_RouteGround.ts`). 같은 선이면 측점 · 지반 ·
|
||||
계획선 · 면적 짝이 모두 같아 누가토량 차가 사라짐(옛 3자리 = 교점 끌기 0.8 ㎥).
|
||||
|
||||
고정 자료 = `fixtures/golden_472` · 브라우저 길 = `helper_472_1_browser.mjs` 옮김 인자 · 서버 길 =
|
||||
`helper_475_1_moved.server_moved`(노선 저장 체인 DB 없이).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_475_1_route_digits.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
import helper_472_1_paths as paths
|
||||
import helper_475_1_moved as moved
|
||||
import pytest
|
||||
|
||||
from B05_Profile.B05_Profile_Router_Replan import _vertices_of
|
||||
from common_util.common_util_route_geometry import write_route_csv
|
||||
|
||||
WALL = {
|
||||
"structure_id": "wall600",
|
||||
"type_id": "retaining_wall",
|
||||
"placement": "interval",
|
||||
"chainage_m": 600.0,
|
||||
"start_m": 595.0,
|
||||
"end_m": 605.0,
|
||||
"options": {
|
||||
"wall_form": "콘크리트",
|
||||
"length_m": 10,
|
||||
"height_m": 2,
|
||||
"before_m": 5,
|
||||
"after_m": 5,
|
||||
"side": "자동(성토 쪽)",
|
||||
},
|
||||
"memo": "",
|
||||
"placement_source": "manual",
|
||||
"status": "draft",
|
||||
"revision": 0,
|
||||
"geometry": None,
|
||||
"summary_item": None,
|
||||
"group_items": None,
|
||||
"x": 208785.614,
|
||||
"y": 367845.1028,
|
||||
}
|
||||
# 교점 9(360 m 언저리) 동 +5 m · 같은 끌기 + 옹벽 595–605 + 추가 측점 510
|
||||
CASES = {
|
||||
"drag": {"node": 9, "dx": 5, "dy": 0},
|
||||
"drag_wall_plus": {"node": 9, "dx": 5, "dy": 0, "plus": [510], "structures": [WALL]},
|
||||
}
|
||||
|
||||
|
||||
def test_geojson_자리_csv_와_같음(tmp_path) -> None:
|
||||
from B05_Profile.B05_Profile_Engine import _route_geojson
|
||||
|
||||
line = [[208785.61234567, 367845.10285, 512.34567], [208790.00005001, 367850.99994999, 1.0]]
|
||||
write_route_csv(tmp_path / "r.csv", [{"x": x, "y": y} for x, y, _ in line])
|
||||
csv_xy = [list(p) for p in _vertices_of(tmp_path / "r.csv")]
|
||||
geo = _route_geojson(line)["geometry"]["coordinates"]
|
||||
assert [[x, y] for x, y, _ in geo] == csv_xy
|
||||
assert geo[0][0] == 208785.6123 and geo[0][2] == 512.346
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=list(CASES))
|
||||
def pair(request, tmp_path_factory) -> tuple[dict, dict]:
|
||||
if not (paths.ROOT / "config" / "node_modules" / "vite").is_dir():
|
||||
pytest.skip("프론트엔드 의존성 미설치")
|
||||
move = copy.deepcopy(CASES[request.param])
|
||||
work = tmp_path_factory.mktemp(request.param)
|
||||
browser = paths.browser(work, move)
|
||||
# 추가 측점 = 브라우저가 새 노선으로 따라간 자리(페이지 [저장]이 보내는 값)
|
||||
if move.get("plus"):
|
||||
move["plus"] = [s["chainage_m"] for s in browser["stations"] if "plus-" in (s["key"] or "")]
|
||||
server = moved.server_moved(
|
||||
paths.stage_project(work / "project"), {**move, "draft": browser["draft"]}
|
||||
)
|
||||
return browser, server
|
||||
|
||||
|
||||
def test_그린_선_서버_선_같음(pair) -> None:
|
||||
browser, server = pair
|
||||
assert browser["route_line"] == server["route_line"]
|
||||
|
||||
|
||||
def test_누가토량_차_002_안(pair) -> None:
|
||||
browser, server = pair
|
||||
b, s = browser["final_cumulative_m3"], server["final_cumulative_m3"]
|
||||
assert browser["station_count"] == server["station_count"]
|
||||
assert abs(b - s) <= 0.02, f"브라우저 {b} · 서버 {s}"
|
||||
Reference in New Issue
Block a user