Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_5
This commit is contained in:
@@ -129,3 +129,17 @@ export function followMovedPipe(projectId: string | null, fromM: number, toM: nu
|
||||
{ partKey: picked.partKey },
|
||||
);
|
||||
}
|
||||
|
||||
/** 종단 측점 하나를 지움(513-3 · 우클릭 「배관 · 구조물 삭제」 종단 · 평면 공용) — 측점 id = 주인 · `주인:역할`
|
||||
* (`plannedIrregular`) · 관 기준 측점 = 그 자리 · 관 구간 끝 = 관 자리 · 그 밖 = 구조물 id(좌측 칸 [삭제]와 같은 길). */
|
||||
export function removeStationOwner(
|
||||
station: { id: string; chainage_m: number },
|
||||
pipe: boolean,
|
||||
remove: { pipe: (chainageM: number) => void; structure: (structureId: string) => void },
|
||||
): void {
|
||||
if (pipe) return remove.pipe(station.chainage_m);
|
||||
const owner = station.id.replace(/:(start|end)$/, "");
|
||||
const at = pipeChainageOf(owner);
|
||||
if (at !== null) remove.pipe(at);
|
||||
else remove.structure(owner);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ import "../../B05_Profile/B05_Profile_UI_Style.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Table.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Drainage.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Structures.css";
|
||||
import { pipeIdAt } from "./design_pipe_ids";
|
||||
import { pipeIdAt, removeStationOwner } from "./design_pipe_ids";
|
||||
import { useDesignStore } from "./design_store";
|
||||
import { pageSettingsPanelValues } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
|
||||
import { setPlusStations } from "../../common_util/common_util_structure_station";
|
||||
@@ -194,9 +194,12 @@ export async function mountProfileView(
|
||||
onStructureMove: (from, to, station) => {
|
||||
if (isPipeStation(station)) profilePanel.drainage.movePipe(from, to);
|
||||
},
|
||||
onStructureRemove: (station) => {
|
||||
if (isPipeStation(station)) profilePanel.drainage.removePipe(station.chainage_m);
|
||||
},
|
||||
// 우클릭 삭제 — 관 = 관 지점 정본 · 그 밖 = 좌측 칸 [삭제]와 같은 길(513-3)
|
||||
onStructureRemove: (station) =>
|
||||
removeStationOwner(station, isPipeStation(station), {
|
||||
pipe: (chainage) => profilePanel.drainage.removePipe(chainage),
|
||||
structure: (structureId) => panel.structures.removeById(structureId),
|
||||
}),
|
||||
// 우클릭 [배관 추가] — 사이드 폼에 종류·측점을 실어 주고 [추가]로 확정(2026-08-18).
|
||||
onPipeAdd: (chainage) => panel.structures.addAt(chainage, "pipe"),
|
||||
onStructureAdd: (chainage, type) =>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
createSheetFollow,
|
||||
fetchRouteSheets,
|
||||
loadRouteEditBackground,
|
||||
contourHintText,
|
||||
type RouteEditContours,
|
||||
type SheetLayers,
|
||||
} from "./B05_Profile_UI_RouteEdit_Contour";
|
||||
@@ -39,7 +40,7 @@ import {
|
||||
} from "./B05_Profile_Api_Replan";
|
||||
import { bindRouteApply } from "./B05_Profile_UI_RouteEdit_Apply";
|
||||
import type { EditedCurve, EditedNode } from "./logic/B05_Profile_UI_RouteEdit_Curve";
|
||||
import { drawRouteEditScene, polylineLengthM, watchTheme } from "./B05_Profile_UI_RouteEdit_Render";
|
||||
import { drawRouteEditScene, routeHeadText, watchTheme } from "./B05_Profile_UI_RouteEdit_Render";
|
||||
import { bindNodeSplice, bindRouteEditNavigation } from "./B05_Profile_UI_RouteEdit_Input";
|
||||
import { bindRouteEditPointer } from "./B05_Profile_UI_RouteEdit_Pointer";
|
||||
import { createCrossPreview } from "./B05_Profile_UI_RouteEdit_Cross";
|
||||
@@ -372,19 +373,10 @@ export async function mountRouteEditor(
|
||||
nodeInfo = built.nodes;
|
||||
}
|
||||
|
||||
/** 상태줄 머리 — 지금 그려진 계획노선 길이와 노드 수(계획서 0-9 ①). 원호가 정점으로
|
||||
* 펴져 있어 브라우저에서 바로 잴 수 있다 — 서버에 묻지 않는다. */
|
||||
/** 상태줄 머리 · 고른 등고선 글 — 짓기는 `_Render` · `_Contour` 몫. */
|
||||
const routeHead = (): string =>
|
||||
`예상노선 ${polylineLengthM(expected).toFixed(1)}m · ` +
|
||||
`계획노선 ${polylineLengthM(plannedLine.length ? plannedLine : planned).toFixed(1)}m · ` +
|
||||
`노드 ${planned.length}개`;
|
||||
|
||||
/** 고른 등고선의 높이 — 못 읽었으면 높이 없이 「고른 등고선」만(계획서 0-9 ⑦). */
|
||||
const contourHint = (): string => {
|
||||
if (pickedContour < 0) return "등고선을 누르면 그 줄의 높이가 보입니다.";
|
||||
const level = contours?.layer.features[pickedContour]?.labelValue ?? null;
|
||||
return level === null ? "등고선 한 줄을 골랐습니다." : `고른 등고선 ${level}m.`;
|
||||
};
|
||||
routeHeadText(expected, plannedLine.length ? plannedLine : planned, planned.length);
|
||||
const contourHint = (): string => contourHintText(contours, pickedContour);
|
||||
|
||||
/** 상태줄 꼬리 — 셈은 `_Edits` 몫. */
|
||||
const curveHint = (): string =>
|
||||
|
||||
@@ -48,6 +48,13 @@ export interface RouteEditContours {
|
||||
minor: (() => PreparedLayer) | null;
|
||||
}
|
||||
|
||||
/** 고른 등고선의 높이 — 못 읽었으면 높이 없이 「고른 등고선」만(계획서 0-9 ⑦). */
|
||||
export function contourHintText(contours: RouteEditContours | null, picked: number): string {
|
||||
if (picked < 0) return "등고선을 누르면 그 줄의 높이가 보입니다.";
|
||||
const level = contours?.layer.features[picked]?.labelValue ?? null;
|
||||
return level === null ? "등고선 한 줄을 골랐습니다." : `고른 등고선 ${level}m.`;
|
||||
}
|
||||
|
||||
interface ContourResponse {
|
||||
contours: Array<{ level: number; coordinates: Array<[number, number, number]> }>;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,16 @@ export function polylineLengthM(points: ReadonlyArray<Vertex>): number {
|
||||
return total;
|
||||
}
|
||||
|
||||
/** 상태줄 머리 — 지금 그려진 계획노선 길이와 노드 수(계획서 0-9 ①). 원호가 정점으로
|
||||
* 펴져 있어 브라우저에서 바로 잴 수 있다 — 서버에 묻지 않는다. */
|
||||
export const routeHeadText = (
|
||||
expected: ReadonlyArray<Vertex>,
|
||||
line: ReadonlyArray<Vertex>,
|
||||
nodeCount: number,
|
||||
): string =>
|
||||
`예상노선 ${polylineLengthM(expected).toFixed(1)}m · ` +
|
||||
`계획노선 ${polylineLengthM(line).toFixed(1)}m · 노드 ${nodeCount}개`;
|
||||
|
||||
export interface RouteEditScene {
|
||||
view: ViewState;
|
||||
/** 사업지 좌표(m) → 캔버스 px. */
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "../A00_Common/design/design_selection";
|
||||
import { paintMapPick } from "../A00_Common/design/design_pick_profile";
|
||||
import { ESCAPE_FROM } from "../A00_Common/design/design_pick_escape";
|
||||
import { removeStationOwner } from "../A00_Common/design/design_pipe_ids";
|
||||
import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
|
||||
import {
|
||||
fetchLatestRoute,
|
||||
@@ -156,7 +157,12 @@ export function createSectionProfile(
|
||||
if (isPipeStation(station)) host.edit()?.movePipe(from, to);
|
||||
},
|
||||
onStructureRemove: (station) => {
|
||||
if (isPipeStation(station)) host.edit()?.removePipe(station.chainage_m);
|
||||
const edit = host.edit();
|
||||
if (edit)
|
||||
removeStationOwner(station, isPipeStation(station), {
|
||||
pipe: edit.removePipe,
|
||||
structure: edit.removeStructure,
|
||||
});
|
||||
},
|
||||
onPipeAdd: (chainage) => host.edit()?.addPipe(chainage),
|
||||
onStructureAdd: (chainage, type) =>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/* 513-3 우클릭 「구조물 삭제」 관 아닌 구조물도(요구 327-1) — `design_pipe_ids.removeStationOwner` 를 Node 로
|
||||
* 돌려 JSON 한 줄(`test_513_3_remove_station_owner.py` 가 판정). 관 장부 · 고름은 가짜 · 주인 가르기는 진짜.
|
||||
* node helper_513_3_remove_station_owner.cjs
|
||||
*/
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
|
||||
const BOOK = { next: 4, ids: [{ id: "pipe-s3", at: 55 }] };
|
||||
const STUBS = {
|
||||
"../b_page_state": { readState: () => BOOK, writeState() {} },
|
||||
"./design_selection": {
|
||||
PIPE_MATCH_M: 0.51,
|
||||
isNear: () => false,
|
||||
nearestWithin: () => null,
|
||||
readSelection: () => null,
|
||||
selectDesign() {},
|
||||
},
|
||||
common_util_pick_tag: { setPipeTagSource() {} },
|
||||
};
|
||||
const { loadTs } = makeLoader({ root: path.join(__dirname, "..", ".."), stubs: STUBS });
|
||||
const ids = loadTs(path.join(__dirname, "..", "..", "A00_Common", "design", "design_pipe_ids.ts"));
|
||||
ids.usePipeIds("p1");
|
||||
|
||||
const calls = [];
|
||||
const remove = {
|
||||
pipe: (m) => calls.push(["pipe", m]),
|
||||
structure: (id) => calls.push(["structure", id]),
|
||||
};
|
||||
const cases = [
|
||||
[{ id: "pipe-s1", chainage_m: 40 }, true], // 관 기준 측점
|
||||
[{ id: "wall-7", chainage_m: 80 }, false], // 구조물 기준 측점
|
||||
[{ id: "wall-8:start", chainage_m: 100 }, false], // 구간 구조물 시작
|
||||
[{ id: "wall-8:end", chainage_m: 130 }, false], // 구간 구조물 끝
|
||||
[{ id: "pipe-s3:end", chainage_m: 60 }, false], // 관 구간 끝 = 장부 자리
|
||||
[{ id: "pipe-12.50:start", chainage_m: 10 }, false], // 옛 자리 이름 관
|
||||
];
|
||||
for (const [station, pipe] of cases) ids.removeStationOwner(station, pipe, remove);
|
||||
process.stdout.write(JSON.stringify({ calls }));
|
||||
@@ -0,0 +1,44 @@
|
||||
"""513-3 종단 · 평면 우클릭 「구조물 삭제」 관 아닌 구조물도(요구 327-1) — 측점 주인을 갈라 관 = 관 지점 정본 ·
|
||||
그 밖 = 좌측 칸 [삭제]와 같은 `removeById`(공용 `removeStationOwner`).
|
||||
|
||||
TS 셈은 `helper_513_3_remove_station_owner.cjs`(Node · 주인 가르기는 진짜).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_513_3_remove_station_owner.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_513_3_remove_station_owner.cjs")
|
||||
|
||||
|
||||
def test_주인대로_지움() -> None:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
assert json.loads(run.stdout)["calls"] == [
|
||||
["pipe", 40],
|
||||
["structure", "wall-7"],
|
||||
["structure", "wall-8"],
|
||||
["structure", "wall-8"],
|
||||
["pipe", 55],
|
||||
["pipe", 12.5],
|
||||
]
|
||||
|
||||
|
||||
def test_두_보기_배선() -> None:
|
||||
view = (ROOT / "A00_Common" / "design" / "design_view_profile.ts").read_text(encoding="utf-8")
|
||||
assert "removeStationOwner(station, isPipeStation(station)" in view
|
||||
assert "panel.structures.removeById(structureId)" in view
|
||||
cross = (ROOT / "B06_Section" / "B06_Section_UI_Section_View_Profile.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "removeStationOwner(station, isPipeStation(station)" in cross
|
||||
assert "structure: edit.removeStructure" in cross
|
||||
Reference in New Issue
Block a user