feat(380-2): 사토장 · 토취장 자리 = 가장 가까운 기존 측점 · 새 측점 안 심음
- common_util_structure_station: 기존 측점(일반 · 구조물 · 추가) · snapExistingStation(가운데면 앞 측점) · snapStationFor(site 만) - 폼 [추가] · 끌기(movedStructure) · 우클릭 추가 = 같은 맞춤 · 폼 칸에 맞춘 측점 다시 씀 - 옛 소수 자동 맞춤 = 기존 측점(끝 소수 · 옛 구조물 측점)에 선 site 안 옮김 - 상세 모달 CAD 글 = 맞춘 측점에서 종류 이름 + 「상세 내용: 사토장 부피」 줄(기존 길 그대로) - 시험 test_380_2_site_station(아무 m 입력 = 가까운 측점 · CAD 글 줄) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -47,11 +47,7 @@ import { showsAnchor } from "./logic/B05_Profile_UI_Structures_Define";
|
||||
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,
|
||||
snapStructureStation,
|
||||
} from "./B05_Profile_UI_Structures_Snap";
|
||||
import { movedStructure, snapOldStations, snapStationFor } from "./B05_Profile_UI_Structures_Snap";
|
||||
|
||||
/** 공개 타입·표시 상수는 700줄 제한으로 `_Structures_Panel_Types` 로 옮겼다 —
|
||||
* 옛 임포트 경로가 그대로 동작하도록 여기서 다시 내보낸다(2026-09-04). */
|
||||
@@ -645,7 +641,7 @@ export function createStructuresSection(
|
||||
loadForm(structures.find((entry) => entry.structure_id === structureId) ?? null);
|
||||
},
|
||||
addAt(chainageM, typeId) {
|
||||
chainageM = snapStructureStation(chainageM); // 정수 m · 일반 측점 붙기(234-1)
|
||||
chainageM = snapStationFor(chainageM, placementOf(typeId)); // 정수 m · site = 기존 측점(234-1 · 380-2)
|
||||
const type = typeMap().get(typeId);
|
||||
if (!type) return;
|
||||
// 우클릭 = [추가]까지 끝난 상태(116-1 · 08-18 「즉시 추가 폐지」를 바꿈) — 새로 놓기에서
|
||||
|
||||
@@ -23,7 +23,10 @@ import { readStructureSpec, specRenderedKeys } from "./B05_Profile_UI_Structures
|
||||
import { asRoad } from "./logic/B05_Profile_UI_Structures_Define";
|
||||
import type { GroupBoxes } from "./B05_Profile_UI_Structures_Groups";
|
||||
import { specFor } from "../A00_Common/design/logic/design_structure_pipe";
|
||||
import { snapStructureStation } from "../common_util/common_util_structure_station";
|
||||
import {
|
||||
snapExistingStation,
|
||||
snapStructureStation,
|
||||
} from "../common_util/common_util_structure_station";
|
||||
import {
|
||||
readGroupItems,
|
||||
readSummaryItem,
|
||||
@@ -328,6 +331,11 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
|
||||
} else {
|
||||
anchor = ctx.anchorFields.read(step, true);
|
||||
if (anchor === null) return stop(ctx.anchorFields.station, "기준 측점");
|
||||
if (placement === "site") {
|
||||
// 사토장 · 토취장 = 친 m 에서 가장 가까운 기존 측점 · 새 측점 안 심음(380-2)
|
||||
anchor = snapExistingStation(anchor);
|
||||
ctx.anchorFields.write(anchor, step);
|
||||
}
|
||||
}
|
||||
// 필수 옵션(미확정 기본값 없음)이 비어 있으면 추가하지 않는다 — 서버도 거절한다.
|
||||
// 상세(detail) 옵션은 폼에 없으므로 여기 걸리지 않는다(B06/B07에서 받는다).
|
||||
|
||||
@@ -11,18 +11,21 @@
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
isSnappedStation,
|
||||
snapExistingStation,
|
||||
snapStationFor,
|
||||
snapStructureStation,
|
||||
} from "../common_util/common_util_structure_station";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
|
||||
export { snapStructureStation };
|
||||
export { snapStationFor, snapStructureStation };
|
||||
import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures";
|
||||
import type { FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility";
|
||||
import type { PipeFacilityItem } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
|
||||
/** 구조물을 그 측점으로 옮긴 모양 — 구간은 기준과의 거리 · 길이를 지키되 시작 · 끝도 맞춤. */
|
||||
/** 구조물을 그 측점으로 옮긴 모양 — 구간은 기준과의 거리 · 길이를 지키되 시작 · 끝도 맞춤 ·
|
||||
* site(사토장 · 토취장) = 가장 가까운 기존 측점(380-2). */
|
||||
export function movedStructure(target: StructureInstance, toChainageM: number): StructureInstance {
|
||||
const anchor = snapStructureStation(toChainageM);
|
||||
const anchor = snapStationFor(toChainageM, target.placement);
|
||||
if (target.placement !== "interval") return { ...target, chainage_m: anchor };
|
||||
const length = (target.end_m ?? 0) - (target.start_m ?? 0);
|
||||
const start = snapStructureStation(anchor - (structureAnchorM(target) - (target.start_m ?? 0)));
|
||||
@@ -47,9 +50,16 @@ export function followSnapped<T extends { chainage_m: number }>(
|
||||
const offGrid = (value: number | null | undefined): boolean =>
|
||||
typeof value === "number" && !isSnappedStation(value);
|
||||
|
||||
/** 기존 측점(끝 소수 · 옛 소수 구조물 측점 포함)에 선 site 인가 — 맞춤 밖으로 안 셈(380-2). */
|
||||
const onExisting = (structure: StructureInstance): boolean =>
|
||||
structure.placement === "site" &&
|
||||
typeof structure.chainage_m === "number" &&
|
||||
snapExistingStation(structure.chainage_m) === structure.chainage_m;
|
||||
|
||||
/** 기준 · 시작 · 끝 중 하나라도 맞춤 밖(소수)인 구조물인가. */
|
||||
export const structureOffGrid = (structure: StructureInstance): boolean =>
|
||||
offGrid(structure.chainage_m) || offGrid(structure.start_m) || offGrid(structure.end_m);
|
||||
!onExisting(structure) &&
|
||||
(offGrid(structure.chainage_m) || offGrid(structure.start_m) || offGrid(structure.end_m));
|
||||
|
||||
export interface SnapPlan {
|
||||
/** 목록 자리 · 옮긴 모양. */
|
||||
|
||||
@@ -40,14 +40,16 @@ const EPS = 1e-6;
|
||||
export const SAME_STATION_M = 0.005;
|
||||
|
||||
let general: number[] = [];
|
||||
let listed: number[] = [];
|
||||
|
||||
/** 일반 측점 목록을 둔다 — 횡단 상세를 읽을 때마다(B05 · B06 한 노선). */
|
||||
/** 일반 측점 목록을 둔다 — 횡단 상세를 읽을 때마다(B05 · B06 한 노선). 구조물 측점까지 든 목록도 둠(380-2). */
|
||||
export function setGeneralStations(
|
||||
stations: ReadonlyArray<{ chainage_m: number; kind?: string }> | null | undefined,
|
||||
): void {
|
||||
general = (stations ?? [])
|
||||
.filter((station) => station.kind !== "irregular")
|
||||
.map((station) => station.chainage_m);
|
||||
listed = (stations ?? []).map((station) => station.chainage_m);
|
||||
}
|
||||
|
||||
export const generalStations = (): readonly number[] => general;
|
||||
@@ -94,6 +96,29 @@ export const isSnappedStation = (
|
||||
stations: readonly number[] = general,
|
||||
): boolean => Math.abs(snapStructureStation(chainageM, stations) - chainageM) < EPS;
|
||||
|
||||
/* ── 기존 측점 맞춤(380-2 · 요구 241-8) — 새 측점을 안 심는 자리 꼴(site · 사토장 · 토취장) ── */
|
||||
|
||||
/** 기존 측점(일반 · 구조물 · 추가) — 오름차순 · 같은 m 하나.
|
||||
* ponytail: 구조물 측점 = 마지막으로 읽은 횡단 상세 것 — 세션에서 새로 놓은 관 측점은 다시 읽을 때까지 빠짐. */
|
||||
export const existingStations = (): number[] =>
|
||||
[...new Set([...listed, ...plusSource()])].sort((a, b) => a - b);
|
||||
|
||||
/** 가장 가까운 기존 측점(가운데면 앞 측점) — 목록이 비면 정수 m 맞춤. 두 번 맞춰도 같다. */
|
||||
export function snapExistingStation(
|
||||
chainageM: number,
|
||||
stations: readonly number[] = existingStations(),
|
||||
): number {
|
||||
if (!Number.isFinite(chainageM) || !stations.length) return snapStructureStation(chainageM);
|
||||
return stations.reduce((best, at) => {
|
||||
const [gap, bestGap] = [Math.abs(at - chainageM), Math.abs(best - chainageM)];
|
||||
return gap < bestGap - EPS || (gap < bestGap + EPS && at < best) ? at : best;
|
||||
});
|
||||
}
|
||||
|
||||
/** 놓는 꼴 따라 맞춤 — site = 가장 가까운 기존 측점 · 그 밖 = 정수 m. */
|
||||
export const snapStationFor = (chainageM: number, placement: string | null | undefined): number =>
|
||||
placement === "site" ? snapExistingStation(chainageM) : snapStructureStation(chainageM);
|
||||
|
||||
/* ── 측점을 심는 자리(272 · 서버 `resolve_extra_stations` · `is_station_planting_type` 짝) ── */
|
||||
|
||||
export interface PlantingPipe {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/* 380-2 사토장 · 토취장(site) = 가장 가까운 기존 측점(일반 · 구조물 · 추가) · 새 측점 안 심음.
|
||||
* 진짜 `common_util_structure_station` · `_Structures_Snap` 을 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 catalog = require("./helper_322_catalog_module.cjs");
|
||||
const station = loadTs("common_util/common_util_structure_station.ts", {
|
||||
"./common_util_structure_catalog": catalog,
|
||||
});
|
||||
const snap = loadTs("B05_Profile/B05_Profile_UI_Structures_Snap.ts", {
|
||||
"@ui/ui_template_elements": { showToast: () => {}, showConfirmDialog: async () => true },
|
||||
"../common_util/common_util_structure_station": station,
|
||||
"../A00_Common/design/design_selection": { PIPE_MATCH_M: 0.51 },
|
||||
"./B05_Profile_Api_Structures": {
|
||||
structureAnchorM: (entry) => entry.chainage_m ?? entry.start_m ?? 0,
|
||||
},
|
||||
});
|
||||
|
||||
const out = {};
|
||||
out.emptySnap = station.snapExistingStation(25.4); // 목록 전 = 정수 m
|
||||
station.setGeneralStations(input.stations);
|
||||
station.setPlusStations(() => input.plus);
|
||||
out.existing = station.existingStations();
|
||||
out.snap = input.typed.map((value) => station.snapExistingStation(value));
|
||||
out.twice = out.snap.map((value) => station.snapExistingStation(value));
|
||||
out.siteFor = station.snapStationFor(25.4, "site");
|
||||
out.pointFor = station.snapStationFor(25.4, "point");
|
||||
out.movedSite = snap.movedStructure({ placement: "site", chainage_m: 20 }, 35.2).chainage_m;
|
||||
out.movedPoint = snap.movedStructure({ placement: "point", chainage_m: 20 }, 35.2).chainage_m;
|
||||
out.offGrid = input.offGrid.map((entry) => snap.structureOffGrid(entry));
|
||||
out.planned = snap
|
||||
.snapPlan(input.offGrid, [])
|
||||
.structures.map((entry) => [entry.index, entry.next.chainage_m]);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""380-2 사토장 · 토취장(site) 자리 = 가장 가까운 기존 측점 · 새 측점 안 심음(요구 241-8).
|
||||
|
||||
기존 측점 = 일반(격자 · 시종점) · 구조물(irregular) · 추가(plus). 아무 m 를 넣어도 그중 가장 가까운
|
||||
측점 · 가운데면 앞 측점 · 목록이 비면 정수 m. 폼 [추가] · 끌기 · 우클릭이 같은 맞춤 ·
|
||||
옛 소수 자동 맞춤은 기존 측점(끝 소수 포함)에 선 site 를 안 옮김.
|
||||
진짜 TS 를 `helper_380_2_site_station.cjs` 가 Node 로 돌린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
STATIONS = [
|
||||
{"chainage_m": 0.0, "kind": "bp"},
|
||||
{"chainage_m": 20.0, "kind": "regular"},
|
||||
{"chainage_m": 33.4, "kind": "irregular"}, # 옛 소수 구조물 측점
|
||||
{"chainage_m": 40.0, "kind": "regular"},
|
||||
{"chainage_m": 60.0, "kind": "regular"},
|
||||
{"chainage_m": 79.8, "kind": "ep"}, # 끝 소수
|
||||
]
|
||||
PLUS = [47.0, 20.0] # 추가 측점(같은 m 는 하나)
|
||||
# (친 m, 기대 측점)
|
||||
TYPED = [
|
||||
(25.0, 20.0),
|
||||
(27.0, 33.4), # 구조물 측점
|
||||
(43.0, 40.0),
|
||||
(44.0, 47.0), # 추가 측점
|
||||
(10.0, 0.0), # 가운데 = 앞 측점
|
||||
(53.5, 47.0), # 가운데 = 앞 측점
|
||||
(78.0, 79.8), # 끝 소수 그대로
|
||||
(1000.0, 79.8), # 노선 밖 = 끝
|
||||
(-5.0, 0.0),
|
||||
(60.0, 60.0),
|
||||
]
|
||||
OFF_GRID = [
|
||||
{"placement": "site", "chainage_m": 79.8}, # 기존 측점(끝 소수) — 안 옮김
|
||||
{"placement": "site", "chainage_m": 33.4}, # 기존 구조물 측점 — 안 옮김
|
||||
{"placement": "site", "chainage_m": 50.3}, # 소수 · 측점 아님 — 가까운 기존 측점으로
|
||||
{"placement": "site", "chainage_m": 55.0}, # 정수 — 옛 규칙대로 안 옮김
|
||||
{"placement": "point", "chainage_m": 79.8}, # site 아님 — 옛 규칙(정수 m)
|
||||
]
|
||||
|
||||
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("s380") / "in.json"
|
||||
source.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"stations": STATIONS,
|
||||
"plus": PLUS,
|
||||
"typed": [value for value, _ in TYPED],
|
||||
"offGrid": OFF_GRID,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_380_2_site_station.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 test_기존_측점_일반_구조물_추가(ts) -> None:
|
||||
assert ts["existing"] == [0.0, 20.0, 33.4, 40.0, 47.0, 60.0, 79.8]
|
||||
|
||||
|
||||
def test_아무_m_입력_가까운_기존_측점(ts) -> None:
|
||||
assert ts["snap"] == [expected for _, expected in TYPED]
|
||||
assert ts["twice"] == ts["snap"] # 두 번 맞춰도 같음
|
||||
assert ts["emptySnap"] == 25 # 목록 전 = 정수 m
|
||||
|
||||
|
||||
def test_site_만_기존_측점_그_밖_정수(ts) -> None:
|
||||
assert ts["siteFor"] == 20 and ts["pointFor"] == 25
|
||||
assert ts["movedSite"] == 33.4 # 끌기 = 가까운 기존 측점
|
||||
assert ts["movedPoint"] == 35
|
||||
|
||||
|
||||
def test_옛_자동_맞춤은_기존_측점_site_안_옮김(ts) -> None:
|
||||
assert ts["offGrid"] == [False, False, True, False, True]
|
||||
assert ts["planned"] == [[2, 47.0], [4, 79.0]] # 끝 79.8 아래 정수
|
||||
|
||||
|
||||
def test_상세_CAD_글_사토장_부피_줄() -> None:
|
||||
"""맞춘 기존 측점 = 상세 모달 CAD 글 측점(±0.51 m) — 종류 이름 + 「사토장 부피」 줄."""
|
||||
import sys
|
||||
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_CulvertNote import (
|
||||
structure_note_lines,
|
||||
with_option_lines,
|
||||
)
|
||||
|
||||
things = with_option_lines(
|
||||
[
|
||||
{
|
||||
"name": "유용토운반작업장(사토장)",
|
||||
"pipe": False,
|
||||
"at": 33.4,
|
||||
"summary_item": None,
|
||||
"group_items": None,
|
||||
"type_id": "spoil_bank",
|
||||
"options": {"capacity_m3": 150},
|
||||
}
|
||||
]
|
||||
)
|
||||
assert structure_note_lines(things, 33.4) == [
|
||||
"유용토운반작업장(사토장)",
|
||||
"상세 내용: 사토장 부피 150㎥",
|
||||
]
|
||||
assert structure_note_lines(things, 40.0) == [] # 이웃 측점엔 없음
|
||||
Reference in New Issue
Block a user