Merge remote-tracking branch 'origin/dev' into sub_laptop_4
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
type AlignmentEdits,
|
||||
} from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { carryProfileEdits, changedSpan } from "./B05_Profile_Grade_Carry";
|
||||
import { SECTION_STATION_INTERVAL_M } from "./B05_Profile_Sections_Stations";
|
||||
import { groundFollowingBasePvi, groundProfile } from "./B05_Profile_Grade_Ground";
|
||||
|
||||
const xy = (line: ReadonlyArray<Vertex>): Array<{ x: number; y: number }> =>
|
||||
@@ -93,9 +94,12 @@ export function createFollowStage(deps: {
|
||||
if (Array.isArray(pipes) && pipes.length)
|
||||
patch.pipes = projectPipePoints(pipes as FollowingPoint[], line);
|
||||
if (Array.isArray(structures) && structures.length)
|
||||
patch.structures = followStructures(structures as FollowingStructure[], line).map(
|
||||
(moved) => moved.item,
|
||||
);
|
||||
patch.structures = followStructures(
|
||||
structures as FollowingStructure[],
|
||||
line,
|
||||
undefined,
|
||||
SECTION_STATION_INTERVAL_M, // site = 새 노선 일반 측점(380-3)
|
||||
).map((moved) => moved.item);
|
||||
const span = ctx.before.route ? changedSpan(xy(before), line) : null;
|
||||
if (span && alignment) patch.alignment = carryProfileEdits([], alignment, span, []).edits;
|
||||
return patch;
|
||||
|
||||
@@ -19,7 +19,11 @@ from typing import Any, Iterable, Sequence
|
||||
|
||||
from shapely.geometry import LineString, Point
|
||||
|
||||
from common_util.common_util_structure_station import snap_structure_station
|
||||
from common_util.common_util_structure_station import (
|
||||
route_general_stations,
|
||||
snap_existing_station,
|
||||
snap_structure_station,
|
||||
)
|
||||
from config.config_system import STRUCTURE_ROUTE_DRIFT_MAX_M
|
||||
|
||||
_POSITION_KEYS = ("chainage_m", "start_m", "end_m")
|
||||
@@ -79,6 +83,7 @@ def follow_structures(
|
||||
|
||||
기준 자리 = 투영 → 정수 m(노선 끝 안) · 구간 끝(`start_m` · `end_m`)은 같은 만큼 밀고 0 ~ 연장
|
||||
안으로 자름. 좌표가 없는 옛 자료는 그대로(거리 None).
|
||||
site 꼴(사토장 · 토취장 · 새 측점 안 심음) = 새 노선 일반 측점 중 가장 가까운 곳(380-3).
|
||||
"""
|
||||
items = list(items)
|
||||
line = _line(xy)
|
||||
@@ -93,7 +98,13 @@ def follow_structures(
|
||||
result.append((None, False))
|
||||
continue
|
||||
point = Point(float(x), float(y))
|
||||
shift = snap_structure_station(float(line.project(point)), [length]) - float(anchor)
|
||||
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)
|
||||
for key in _POSITION_KEYS:
|
||||
value = getattr(item, key, None)
|
||||
if value is not None:
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* 거울 시험 = resources/tester/test_344_2_route_pairs.py.
|
||||
* ========================================================================== */
|
||||
|
||||
import { SAME_STATION_M, snapStructureStation } from "./common_util_structure_station";
|
||||
import {
|
||||
gridStations,
|
||||
SAME_STATION_M,
|
||||
snapExistingStation,
|
||||
snapStructureStation,
|
||||
} from "./common_util_structure_station";
|
||||
|
||||
/** 「노선에서 멀어짐」 거리(m) — 파이썬 `config_system.STRUCTURE_ROUTE_DRIFT_MAX_M` 짝(거울 시험이 대조). */
|
||||
export const STRUCTURE_ROUTE_DRIFT_MAX_M = 20;
|
||||
@@ -146,6 +151,7 @@ export function projectPipePoints<P extends FollowingPoint>(
|
||||
}
|
||||
|
||||
export interface FollowingStructure {
|
||||
placement?: string | null;
|
||||
chainage_m?: number | null;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
@@ -153,18 +159,24 @@ export interface FollowingStructure {
|
||||
y?: number | null;
|
||||
}
|
||||
|
||||
/** 구조물이 좌표를 따라감 — 사본 · 노선과 거리(좌표 없으면 null) · 멀어짐(차례는 그대로). */
|
||||
/** 구조물이 좌표를 따라감 — 사본 · 노선과 거리(좌표 없으면 null) · 멀어짐(차례는 그대로) ·
|
||||
* `intervalM` 주면 site 꼴(사토장 · 토취장) = 새 노선 일반 측점 중 가장 가까운 곳(380-3 · 파이썬은 서버 간격). */
|
||||
export function followStructures<S extends FollowingStructure>(
|
||||
items: ReadonlyArray<S>,
|
||||
line: ReadonlyArray<XY>,
|
||||
maxDriftM: number = STRUCTURE_ROUTE_DRIFT_MAX_M,
|
||||
intervalM: number | null = null,
|
||||
): Array<{ item: S; drift_m: number | null; far: boolean }> {
|
||||
return items.map((item) => {
|
||||
const hit =
|
||||
item.x != null && item.y != null ? projectOnPolyline(line, { x: item.x, y: item.y }) : null;
|
||||
if (hit === null || item.chainage_m == null)
|
||||
return { item: { ...item }, drift_m: null, far: false };
|
||||
const shift = snapStructureStation(hit.chainage_m, [hit.length_m]) - item.chainage_m;
|
||||
const target =
|
||||
intervalM && item.placement === "site"
|
||||
? snapExistingStation(hit.chainage_m, gridStations(hit.length_m, intervalM))
|
||||
: snapStructureStation(hit.chainage_m, [hit.length_m]);
|
||||
const shift = target - item.chainage_m;
|
||||
const clamp = (value: number): number => Math.min(Math.max(value + shift, 0), hit.length_m);
|
||||
return {
|
||||
item: {
|
||||
|
||||
@@ -45,6 +45,14 @@ def snap_structure_station(chainage_m: float, stations: Sequence[float]) -> floa
|
||||
return max(0.0, min(whole, float(math.floor(end + _EPS))))
|
||||
|
||||
|
||||
def snap_existing_station(chainage_m: float, stations: Sequence[float]) -> float:
|
||||
"""가장 가까운 기존 측점(가운데면 앞) · 비면 정수 m — TS `snapExistingStation` 짝."""
|
||||
value = float(chainage_m)
|
||||
if not math.isfinite(value) or not stations:
|
||||
return snap_structure_station(value, stations)
|
||||
return float(min(stations, key=lambda at: (round(abs(float(at) - value), 6), float(at))))
|
||||
|
||||
|
||||
def route_general_stations(total_m: float) -> list[float]:
|
||||
"""노선 길이의 일반 측점 — 서버 측점 간격(`SECTION_STATION_INTERVAL_M`)."""
|
||||
return general_stations(total_m, SECTION_STATION_INTERVAL_M)
|
||||
|
||||
@@ -115,6 +115,17 @@ export function snapExistingStation(
|
||||
});
|
||||
}
|
||||
|
||||
/** 격자 측점 + 끝 측점 — 파이썬 `general_stations` 짝(노선 따라감 site 맞춤 · 380-3). */
|
||||
export function gridStations(totalM: number, intervalM: number): number[] {
|
||||
if (!(totalM > 0 && intervalM > 0)) return [0];
|
||||
const values = Array.from(
|
||||
{ length: Math.floor(totalM / intervalM) + 1 },
|
||||
(_, i) => i * intervalM,
|
||||
);
|
||||
if (totalM - values[values.length - 1] > EPS) values.push(totalM);
|
||||
return values;
|
||||
}
|
||||
|
||||
/** 놓는 꼴 따라 맞춤 — site = 가장 가까운 기존 측점 · 그 밖 = 정수 m. */
|
||||
export const snapStationFor = (chainageM: number, placement: string | null | undefined): number =>
|
||||
placement === "site" ? snapExistingStation(chainageM) : snapStructureStation(chainageM);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/* 380-3 노선 따라간 사토장 · 토취장(site) = 새 노선 일반 측점 중 가장 가까운 곳 — 진짜
|
||||
* `common_util_route_follow` · `common_util_structure_station` 을 Node 로 돌림. 입력 argv[2] JSON · 표준출력 JSON.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
function loadTs(file, stubs = {}) {
|
||||
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, file), "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const localRequire = (request) => {
|
||||
if (stubs[request]) return stubs[request];
|
||||
throw new Error(`stub 없음: ${request} (${file})`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const station = loadTs("common_util/common_util_structure_station.ts", {
|
||||
"./common_util_structure_catalog": require("./helper_322_catalog_module.cjs"),
|
||||
});
|
||||
const follow = loadTs("common_util/common_util_route_follow.ts", {
|
||||
"./common_util_structure_station": station,
|
||||
});
|
||||
const out = {
|
||||
grid: input.grids.map(([total, interval]) => station.gridStations(total, interval)),
|
||||
snap: input.snaps.map(([value, stations]) => station.snapExistingStation(value, stations)),
|
||||
follow: follow
|
||||
.followStructures(input.items, input.line, undefined, input.interval)
|
||||
.map((entry) => entry.item.chainage_m),
|
||||
plain: follow.followStructures(input.items, input.line).map((entry) => entry.item.chainage_m),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -182,7 +182,8 @@ def test_save_writes_list_and_enabled_together(ts: dict) -> None:
|
||||
assert sync["same"] is True
|
||||
rows = catalog()
|
||||
assert set(sync["enabled"]) == {t for t, row in rows.items() if row["list"] == "both"}
|
||||
assert len(sync["enabled"]) == 23 # B05 목록 23(브레인 화면 확인)
|
||||
# 개수는 마스터 판(M02 사용자 켬끔)마다 다름 — 고정 수 대신 B05 목록(both) 수와 같음만(380-3)
|
||||
assert len(sync["enabled"]) == sum(1 for row in rows.values() if row["list"] == "both") > 0
|
||||
assert ts["partOff"] == [{"형상.wall": False}, "none"] and ts["partOn"] == {}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""380-3 노선이 바뀌어 좌표로 따라간 사토장 · 토취장(site) = 새 노선 일반 측점 중 가장 가까운 곳.
|
||||
|
||||
요구 241-8 「기존에 있던 측점들을 기준으로」 — 따라간 뒤에도 기존 측점에 섬(CAD 글 ±0.51 m).
|
||||
그 밖 꼴은 종전대로 정수 m. 짝 = 파이썬 `follow_structures` · `snap_existing_station` ·
|
||||
`general_stations` ↔ TS `followStructures(…, intervalM)` · `snapExistingStation` · `gridStations`
|
||||
(`helper_380_3_site_follow.cjs`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from common_util.common_util_route_follow import follow_structures # noqa: E402
|
||||
from common_util.common_util_structure_station import ( # noqa: E402
|
||||
general_stations,
|
||||
snap_existing_station,
|
||||
)
|
||||
from config.config_system_design import SECTION_STATION_INTERVAL_M # noqa: E402
|
||||
|
||||
# 새 노선 = 길이 130.0 m(50 + 50 + 30) — 일반 측점 0 · 20 · … · 120 · 130
|
||||
LINE = [(0.0, 0.0), (50.0, 0.0), (50.0, 50.0), (80.0, 50.0)]
|
||||
ITEMS = [
|
||||
{"placement": "site", "chainage_m": 33.0, "x": 33.0, "y": 1.0}, # 투영 33 → 40
|
||||
{"placement": "site", "chainage_m": 75.0, "x": 51.0, "y": 25.0}, # 투영 75 → 80(가까운 쪽)
|
||||
{"placement": "site", "chainage_m": 10.0, "x": 10.0, "y": -2.0}, # 가운데 10 → 앞 측점 0
|
||||
{"placement": "site", "chainage_m": 127.0, "x": 78.0, "y": 50.0}, # 투영 128 → 끝 130
|
||||
{"placement": "point", "chainage_m": 33.0, "x": 33.0, "y": 1.0}, # site 아님 = 정수 m 33
|
||||
{"placement": "site", "chainage_m": 40.0}, # 좌표 없음 — 그대로
|
||||
]
|
||||
WANT = [40.0, 80.0, 0.0, 130.0, 33.0, 40.0]
|
||||
GRIDS = [(130.0, 20.0), (120.0, 20.0), (0.0, 20.0), (5.5, 2.0)]
|
||||
SNAPS = [(25.0, [0.0, 20.0, 40.0]), (30.0, [0.0, 20.0, 40.0]), (7.0, [0.0, 10.0])]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts(tmp_path_factory) -> dict:
|
||||
source = tmp_path_factory.mktemp("s3803") / "in.json"
|
||||
source.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"grids": GRIDS,
|
||||
"snaps": SNAPS,
|
||||
"items": ITEMS,
|
||||
"line": [{"x": x, "y": y} for x, y in LINE],
|
||||
"interval": SECTION_STATION_INTERVAL_M,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_380_3_site_follow.cjs"), str(source)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=str(ROOT),
|
||||
check=False,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _python_follow() -> list[float]:
|
||||
items = [
|
||||
SimpleNamespace(**{"x": None, "y": None, "start_m": None, "end_m": None, **item})
|
||||
for item in ITEMS
|
||||
]
|
||||
follow_structures(items, LINE)
|
||||
return [item.chainage_m for item in items]
|
||||
|
||||
|
||||
def test_따라간_site_는_새_노선_일반_측점() -> None:
|
||||
assert SECTION_STATION_INTERVAL_M == 20.0
|
||||
assert _python_follow() == WANT
|
||||
|
||||
|
||||
def test_거울_파이썬_TS(ts) -> None:
|
||||
assert ts["follow"] == _python_follow()
|
||||
assert ts["grid"] == [general_stations(total, interval) for total, interval in GRIDS]
|
||||
assert ts["snap"] == [snap_existing_station(value, stations) for value, stations in SNAPS]
|
||||
assert ts["snap"] == [20.0, 20.0, 10.0] # 가운데면 앞 측점
|
||||
|
||||
|
||||
def test_간격_안_주면_종전_정수_m(ts) -> None:
|
||||
assert ts["plain"] == [33.0, 75.0, 10.0, 128.0, 33.0, 40.0]
|
||||
Reference in New Issue
Block a user