Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1
design_page.ts import 충돌 = 424 의 좌우 이동 import 지움 + 428 pipelineDetail 살림 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
This commit is contained in:
@@ -32,7 +32,7 @@ import { saveSharedDesign, type DesignPageShared } from "./design_shared";
|
|||||||
import { guardDesignLeave } from "./design_leave";
|
import { guardDesignLeave } from "./design_leave";
|
||||||
import { usePipeIds } from "./design_pipe_ids";
|
import { usePipeIds } from "./design_pipe_ids";
|
||||||
import { pinDraftProject } from "./design_store";
|
import { pinDraftProject } from "./design_store";
|
||||||
import { reconcilePagePipeline } from "./design_pipeline_page";
|
import { pipelineDetail, reconcilePagePipeline } from "./design_pipeline_page";
|
||||||
import { forgetPageData, onPageBusy } from "./design_page_data";
|
import { forgetPageData, onPageBusy } from "./design_page_data";
|
||||||
import { pageRouteId } from "./design_page_route";
|
import { pageRouteId } from "./design_page_route";
|
||||||
import { setPipeSectionsSource } from "./design_structure_pipe_calc";
|
import { setPipeSectionsSource } from "./design_structure_pipe_calc";
|
||||||
@@ -115,9 +115,12 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
|||||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||||
pinDraftProject(projectId); // 연 프로젝트 고정(388-3) — 다른 탭이 바꿔도 이 탭 초안 칸 그대로
|
pinDraftProject(projectId); // 연 프로젝트 고정(388-3) — 다른 탭이 바꿔도 이 탭 초안 칸 그대로
|
||||||
usePipeIds(projectId || null); // 관 세션 id 장부 = 이 프로젝트(323-5 · 카드 · 알약 · 종단 · 3D 같은 id)
|
usePipeIds(projectId || null); // 관 세션 id 장부 = 이 프로젝트(323-5 · 카드 · 알약 · 종단 · 3D 같은 id)
|
||||||
// 관 길이 · 흙막이 개수 = 공용 캐시(페이지 노선) 한 벌 — 마지막 연 보기의 사본이 아님(416 · B05 = B06)
|
// 관 길이 · 흙막이 개수 = 공용 캐시(페이지 노선) 한 벌 — 마지막 연 보기의 사본이 아님(416 · B05 = B06) ·
|
||||||
|
// 노선 초안 동안 = 계산 줄 새 노선 상세(다시 센 횡단 · 428)
|
||||||
setPipeSectionsSource(
|
setPipeSectionsSource(
|
||||||
() => peekSectionDetail(projectId, pageRouteId(projectId))?.cross_sections ?? null,
|
() =>
|
||||||
|
(pipelineDetail(projectId) ?? peekSectionDetail(projectId, pageRouteId(projectId)))
|
||||||
|
?.cross_sections ?? null,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 한 벌 틀 — 좌측 자리 · 가운데(위 칸 + 아래 종단도). 진행 단계 틀은 단계 상태가 오면 감싼다.
|
// 한 벌 틀 — 좌측 자리 · 가운데(위 칸 + 아래 종단도). 진행 단계 틀은 단계 상태가 오면 감싼다.
|
||||||
|
|||||||
@@ -366,6 +366,9 @@ def _raw_payload(
|
|||||||
**point.as_dict(),
|
**point.as_dict(),
|
||||||
"chainage_m": round(pipe.chainage_m, 2),
|
"chainage_m": round(pipe.chainage_m, 2),
|
||||||
"lonlat": list(to_lonlat(pipe.x, pipe.y)),
|
"lonlat": list(to_lonlat(pipe.x, pipe.y)),
|
||||||
|
# 노선 위 자리(사업지 CRS) — 화면이 따라가기만 한 관을 가림(rebasePipeDraft · 428)
|
||||||
|
"x": round(float(pipe.x), 3),
|
||||||
|
"y": round(float(pipe.y), 3),
|
||||||
"source": pipe.reason,
|
"source": pipe.reason,
|
||||||
}
|
}
|
||||||
for pipe, point in zip(detail.pipes, points)
|
for pipe, point in zip(detail.pipes, points)
|
||||||
@@ -568,6 +571,20 @@ async def delete_pipe_points(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
|||||||
return _payload(project_id, context, detail, points, False, stale)
|
return _payload(project_id, context, detail, points, False, stale)
|
||||||
|
|
||||||
|
|
||||||
|
async def _stand_pipe_stations(project_id: UUID) -> None:
|
||||||
|
"""관 PUT 끝 — 측점을 지금 관 자리로 다시 세움(`put_plus_stations` 처럼 · 부르는 길 모두 · 428).
|
||||||
|
|
||||||
|
노선 [저장] 뒤 관 차례가 화면 누가거리로 관을 덮으면 서버가 노선 저장 때 세운 측점과 어긋남 —
|
||||||
|
여기서 맞추지 않으면 「측점이 없는 구조물」 · 옛 자리 카드가 남음. 실패해도 관 저장은 그대로.
|
||||||
|
"""
|
||||||
|
from B06_Section.B06_Section_Router_Stations import ensure_structure_stations
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_structure_stations(project_id)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("관 저장: 측점 맞춤 실패(저장은 그대로): project_id=%s", project_id)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{project_id}/drainage/pipe-points", response_model=None)
|
@router.put("/{project_id}/drainage/pipe-points", response_model=None)
|
||||||
async def put_pipe_points(
|
async def put_pipe_points(
|
||||||
project_id: UUID,
|
project_id: UUID,
|
||||||
@@ -610,6 +627,7 @@ async def put_pipe_points(
|
|||||||
failed = await asyncio.to_thread(
|
failed = await asyncio.to_thread(
|
||||||
safe_sync, resolve_stored_project_path(context.stored_path), importer(session)
|
safe_sync, resolve_stored_project_path(context.stored_path), importer(session)
|
||||||
)
|
)
|
||||||
|
await _stand_pipe_stations(project_id)
|
||||||
result = _payload(project_id, context, detail, points, True, stale)
|
result = _payload(project_id, context, detail, points, True, stale)
|
||||||
result["saved_count"] = saved
|
result["saved_count"] = saved
|
||||||
result["snapshot_failed"] = failed # 못 만든 구조물 조합 복사본(142-2) — [저장] 알림
|
result["snapshot_failed"] = failed # 못 만든 구조물 조합 복사본(142-2) — [저장] 알림
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ import { useStationKeys } from "./B06_Section_Station_Keys";
|
|||||||
import { hasStaleDesigns, staleKey } from "./B06_Section_UI_Section_Common";
|
import { hasStaleDesigns, staleKey } from "./B06_Section_UI_Section_Common";
|
||||||
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
|
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
|
||||||
import { withPipeOptions } from "./B06_Section_Section_Adjusts";
|
import { withPipeOptions } from "./B06_Section_Section_Adjusts";
|
||||||
|
import { extendUnclosed } from "../B05_Profile/logic/B05_Profile_Sections_Ground";
|
||||||
|
import { designTerrainGridNow } from "../B05_Profile/B05_Profile_Api_TerrainGrid";
|
||||||
|
|
||||||
/** 계획선 편집 델타 — B05 `AlignmentEdits`와 저장분 `profile_alignment.edits`가 같은 모양이다. */
|
/** 계획선 편집 델타 — B05 `AlignmentEdits`와 저장분 `profile_alignment.edits`가 같은 모양이다. */
|
||||||
export interface CrossRefreshEdits {
|
export interface CrossRefreshEdits {
|
||||||
@@ -367,6 +369,8 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
|||||||
return Math.max(session ?? (typeof stored === "number" ? stored : 0), 0);
|
return Math.max(session ?? (typeof stored === "number" ? stored : 0), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 미교차 넓힘 격자 — 받기 전이면 null(받은 뒤 다시 셈이 넓힘 · 지문에 실음)
|
||||||
|
const grid = designTerrainGridNow(projectId)?.grid ?? null;
|
||||||
const updated: number[] = [];
|
const updated: number[] = [];
|
||||||
for (const section of detail.cross_sections) {
|
for (const section of detail.cross_sections) {
|
||||||
const previous = section.design;
|
const previous = section.design;
|
||||||
@@ -432,7 +436,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
|||||||
// 국부 연산(346-2) — 입력 지문(지반 표본 · 계획고 · 횡단 입력 · 구조물 맥락 · 표준값)이 같고
|
// 국부 연산(346-2) — 입력 지문(지반 표본 · 계획고 · 횡단 입력 · 구조물 맥락 · 표준값)이 같고
|
||||||
// 설계가 그때 낸 그 객체면 앞 결과 그대로. 계획고 ▲ 한 번에 다시 세는 측점은 곡선 둘레 몇 곳뿐.
|
// 설계가 그때 낸 그 객체면 앞 결과 그대로. 계획고 ▲ 한 번에 다시 세는 측점은 곡선 둘레 몇 곳뿐.
|
||||||
const samples = section.samples ?? [];
|
const samples = section.samples ?? [];
|
||||||
const fingerprint = `${standardKey}|${elevation}|${samples.length}|${JSON.stringify({ ...options, standard: null })}`;
|
const fingerprint = `${standardKey}|${elevation}|${samples.length}|${grid ? "grid" : ""}|${JSON.stringify({ ...options, standard: null })}`;
|
||||||
const memo = memos.get(section);
|
const memo = memos.get(section);
|
||||||
if (
|
if (
|
||||||
memo?.samples === section.samples &&
|
memo?.samples === section.samples &&
|
||||||
@@ -444,6 +448,21 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
|||||||
let next;
|
let next;
|
||||||
try {
|
try {
|
||||||
next = computeCrossDesign(samples, elevation, options);
|
next = computeCrossDesign(samples, elevation, options);
|
||||||
|
// 미교차 넓힘 — 서버 [저장] `extend_unclosed_sections` 짝 · 넓힌 표본을 측점에 남김(428 · 묶음 cross 와 같음)
|
||||||
|
const wide =
|
||||||
|
grid && next.slope_unclosed
|
||||||
|
? extendUnclosed(
|
||||||
|
samples,
|
||||||
|
[section.center_x, section.center_y],
|
||||||
|
section.frame.left_xy,
|
||||||
|
(more) => computeCrossDesign(more as typeof samples, elevation, options),
|
||||||
|
grid,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
if (wide && (wide.info.left_m || wide.info.right_m)) {
|
||||||
|
section.samples = wide.samples as typeof samples;
|
||||||
|
next = computeCrossDesign(section.samples, elevation, options);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
continue; // 샘플 부족·값 손상 측점은 건너뛴다(서버 엔진과 같은 태도).
|
continue; // 샘플 부족·값 손상 측점은 건너뛴다(서버 엔진과 같은 태도).
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,15 @@
|
|||||||
* 대신 **왜 값이 없는지**가 화면에 남는다.
|
* 대신 **왜 값이 없는지**가 화면에 남는다.
|
||||||
* ⚠ 지표 샘플링 조건이 저장에 없으면 **단추를 잠그고 사유를 보인다** — 조건을 지어내면
|
* ⚠ 지표 샘플링 조건이 저장에 없으면 **단추를 잠그고 사유를 보인다** — 조건을 지어내면
|
||||||
* 그 측점만 다른 지표에서 뽑혀 옆 측점과 지반고가 어긋난다.
|
* 그 측점만 다른 지표에서 뽑혀 옆 측점과 지반고가 어긋난다.
|
||||||
|
* [저장] 뒤 다시 봄(428) — 관 · 구조물 초안이 정본으로 나가 비면 저장이 끝난 뒤 한 번 더 묻고 줄을 갈음
|
||||||
|
* (서버가 관 PUT 끝에 측점을 다시 세움 · 옛 알림이 남지 않게).
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { API_BASE_URL } from "@config/config_frontend";
|
import { API_BASE_URL } from "@config/config_frontend";
|
||||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||||
|
import { useDesignStore } from "../A00_Common/design/design_store";
|
||||||
|
import { savingDesign } from "../A00_Common/design/design_save";
|
||||||
|
|
||||||
interface MissingStation {
|
interface MissingStation {
|
||||||
chainage_m: number;
|
chainage_m: number;
|
||||||
@@ -49,12 +53,14 @@ export async function mountMissingStationNotice(
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
onCreated: () => void | Promise<void>,
|
onCreated: () => void | Promise<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
watchSaves(host, projectId, onCreated);
|
||||||
let data: MissingResponse;
|
let data: MissingResponse;
|
||||||
try {
|
try {
|
||||||
data = await call(projectId, "GET");
|
data = await call(projectId, "GET");
|
||||||
} catch {
|
} catch {
|
||||||
return; // 점검이 안 되는 것으로 화면을 막지 않는다 — 이 줄은 덤이다.
|
return; // 점검이 안 되는 것으로 화면을 막지 않는다 — 이 줄은 덤이다.
|
||||||
}
|
}
|
||||||
|
host.querySelector(":scope > .b06-missing-stations")?.remove(); // 다시 봄 = 줄 하나
|
||||||
const missing = data.missing ?? [];
|
const missing = data.missing ?? [];
|
||||||
if (!missing.length) return;
|
if (!missing.length) return;
|
||||||
|
|
||||||
@@ -99,3 +105,27 @@ export async function mountMissingStationNotice(
|
|||||||
}
|
}
|
||||||
host.prepend(box);
|
host.prepend(box);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const watched = new WeakSet<HTMLElement>();
|
||||||
|
|
||||||
|
/** 관 · 구조물 초안이 비면(저장 · 되돌림) 저장이 끝난 뒤 다시 물음 — 자리(`host`)가 떠나면 끊음. */
|
||||||
|
function watchSaves(
|
||||||
|
host: HTMLElement,
|
||||||
|
projectId: string,
|
||||||
|
onCreated: () => void | Promise<void>,
|
||||||
|
): void {
|
||||||
|
if (watched.has(host)) return;
|
||||||
|
watched.add(host);
|
||||||
|
const store = useDesignStore(projectId, null);
|
||||||
|
let timer = 0;
|
||||||
|
// ponytail: 저장 끝 신호가 없어 0.3 s 마다 봄 — 저장 끝 알림 창구가 생기면 그리로
|
||||||
|
const recheck = (): void => {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
if (!host.isConnected) return stops.forEach((stop) => stop());
|
||||||
|
if (savingDesign(projectId)) timer = window.setTimeout(recheck, 300);
|
||||||
|
else void mountMissingStationNotice(host, projectId, onCreated);
|
||||||
|
};
|
||||||
|
const stops = (["pipes", "structures"] as const).map((key) =>
|
||||||
|
store.subscribe(key, (value) => value === null && recheck()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
* · 같은 측점(열쇠 · 없으면 같은 자리 같은 종류)은 앞 횡단을 이어 받음 — 설계 · 구조물 제원 · 이름 그대로 ·
|
* · 같은 측점(열쇠 · 없으면 같은 자리 같은 종류)은 앞 횡단을 이어 받음 — 설계 · 구조물 제원 · 이름 그대로 ·
|
||||||
* 자리(누가거리 · 좌표 · 방위 · 확폭)와 지반 표본(채워 왔으면)만 새것.
|
* 자리(누가거리 · 좌표 · 방위 · 확폭)와 지반 표본(채워 왔으면)만 새것.
|
||||||
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
|
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
|
||||||
|
* · 상단측 = 새 지반 표본으로 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
|
||||||
|
* · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자가 만진 칸만 — 서버가 그 행을 지우고 기본 설계에
|
||||||
|
* [저장] 수정분을 얹는 것과 같음(`sync_cross_section_rows` · `compute_default_designs` · 428).
|
||||||
* · 빠진 측점 = 목록에서 뺌(세션 고침은 열쇠로 남음 · 노선이 바뀐 때만 「노선 밖 고침」 · 부르는 쪽).
|
* · 빠진 측점 = 목록에서 뺌(세션 고침은 열쇠로 남음 · 노선이 바뀐 때만 「노선 밖 고침」 · 부르는 쪽).
|
||||||
* · 옛 상세는 그대로 둠 — 새 객체를 돌려줌(쥔 쪽이 갈아끼움).
|
* · 옛 상세는 그대로 둠 — 새 객체를 돌려줌(쥔 쪽이 갈아끼움).
|
||||||
*
|
*
|
||||||
@@ -15,6 +18,7 @@
|
|||||||
|
|
||||||
import type { PipelineStation } from "../../A00_Common/design/logic/design_pipeline_types";
|
import type { PipelineStation } from "../../A00_Common/design/logic/design_pipeline_types";
|
||||||
import type { CrossSection, SectionDetailResponse, SectionStation } from "../B06_Section_Api_Fetch";
|
import type { CrossSection, SectionDetailResponse, SectionStation } from "../B06_Section_Api_Fetch";
|
||||||
|
import { uphillSide } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||||
|
|
||||||
interface Sample {
|
interface Sample {
|
||||||
offset_m: number;
|
offset_m: number;
|
||||||
@@ -51,15 +55,36 @@ export function formatStation(chainageM: number): string {
|
|||||||
export const stationIdOf = (chainageM: number): string =>
|
export const stationIdOf = (chainageM: number): string =>
|
||||||
`station_${String(Math.round(chainageM * 1000)).padStart(12, "0")}`;
|
`station_${String(Math.round(chainageM * 1000)).padStart(12, "0")}`;
|
||||||
|
|
||||||
/** 새 측점의 기본 설계 — 계획고 0 이라 늘 낡음 → 다시 셈이 세션 선택 · 기본값으로 채움. */
|
/** 새 측점의 기본 설계 — 계획고 0 이라 늘 낡음 → 다시 셈이 세션 선택 · 기본값으로 채움 · 단면 유형 = 상단측 깎기. */
|
||||||
const freshDesign = () =>
|
const freshDesign = (uphill: CrossSection["uphill_side"]) =>
|
||||||
({
|
({
|
||||||
design_elevation_m: 0,
|
design_elevation_m: 0,
|
||||||
ground_type: "ripping_rock",
|
ground_type: "ripping_rock",
|
||||||
section_mode: "left_cut",
|
section_mode: `${uphill ?? "left"}_cut`,
|
||||||
ditch_type: "standard",
|
ditch_type: "standard",
|
||||||
}) as unknown as NonNullable<CrossSection["design"]>;
|
}) as unknown as NonNullable<CrossSection["design"]>;
|
||||||
|
|
||||||
|
/** 서버가 행을 지우고 새로 세우는 옮김 폭(m) — `sync_cross_section_rows` 의 0.5 m 짝. */
|
||||||
|
const FACILITY_RESEED_M = 0.5;
|
||||||
|
/** 다시 셈 입력 중 기본 설계가 정하는 칸 — 옮긴 시설 측점은 버림(세션 고침은 열쇠째 옮겨 다시 얹힘). */
|
||||||
|
const AUTO_INPUT_KEYS = [
|
||||||
|
"ditch_side",
|
||||||
|
"rock_boundary_offset_m",
|
||||||
|
"two_stage_slope",
|
||||||
|
"ditch_choice",
|
||||||
|
"ditch_enabled",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 옮긴 시설 측점 설계 — 기본 설계 위에 앞 설계의 나머지(사용자가 만진 칸 · 결과 칸은 다시 셈이 갈음). */
|
||||||
|
function reseeded(
|
||||||
|
prior: CrossSection["design"],
|
||||||
|
uphill: CrossSection["uphill_side"],
|
||||||
|
): NonNullable<CrossSection["design"]> {
|
||||||
|
const design = { ...prior, ...freshDesign(uphill) } as Record<string, unknown>;
|
||||||
|
for (const key of AUTO_INPUT_KEYS) delete design[key];
|
||||||
|
return design as unknown as NonNullable<CrossSection["design"]>;
|
||||||
|
}
|
||||||
|
|
||||||
function kindOf(station: PipelineStation): SectionStation["kind"] {
|
function kindOf(station: PipelineStation): SectionStation["kind"] {
|
||||||
if (station.kind === "end") return station.key.endsWith("EP") ? "ep" : "bp";
|
if (station.kind === "end") return station.key.endsWith("EP") ? "ep" : "bp";
|
||||||
if (station.kind === "curve") return "curve";
|
if (station.kind === "curve") return "curve";
|
||||||
@@ -169,7 +194,18 @@ export function swapCrossSections(
|
|||||||
};
|
};
|
||||||
longStations.push({ ...place, ...(prior?.structure ? { structure: prior.structure } : {}) });
|
longStations.push({ ...place, ...(prior?.structure ? { structure: prior.structure } : {}) });
|
||||||
const samples = station.ground ? groundSamples(station) : (prior?.samples ?? []);
|
const samples = station.ground ? groundSamples(station) : (prior?.samples ?? []);
|
||||||
return prior ? { ...prior, ...place, samples } : { ...place, samples, design: freshDesign() };
|
const uphill = station.ground ? uphillSide(station) : (prior?.uphill_side ?? null);
|
||||||
|
if (!prior) return { ...place, uphill_side: uphill, samples, design: freshDesign(uphill) };
|
||||||
|
const reseed =
|
||||||
|
(station.kind === "pipe" || station.kind === "structure") &&
|
||||||
|
Math.abs(prior.chainage_m - station.chainageM) > FACILITY_RESEED_M;
|
||||||
|
return {
|
||||||
|
...prior,
|
||||||
|
...place,
|
||||||
|
uphill_side: uphill,
|
||||||
|
samples,
|
||||||
|
...(reseed ? { design: reseeded(prior.design, uphill) } : {}),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
const removed = detail.cross_sections
|
const removed = detail.cross_sections
|
||||||
.map((section, i) => (used.has(section) ? null : oldKeys[i]))
|
.map((section, i) => (used.has(section) ? null : oldKeys[i]))
|
||||||
|
|||||||
@@ -107,8 +107,9 @@ export function placeOnLine<S extends FollowingStructure & { id?: string | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 노선을 저장한 [저장] — 세션 관 초안(목록 한 벌)을 서버가 따라 옮긴 관(`after`) 위에 얹은 목록(누가거리 순).
|
/** 노선을 저장한 [저장] — 세션 관 초안(목록 한 벌)을 서버가 따라 옮긴 관(`after`) 위에 얹은 목록(누가거리 순).
|
||||||
* 초안 관이 노선 저장 전 정본(`before`) 자리 그대로면(따라감 전 값) 서버 자리로(구간 끝 · 좌표도) · 사용자가
|
* 초안 관이 노선 저장 전 정본(`before`) 자리 그대로면(따라감 전 값) · 따라가기만 했으면(땅 위 자리 = 정본 x · y ·
|
||||||
* 옮긴 · 새로 놓은 관은 그대로 · 서버가 새로 놓은 관(옛 정본에 없던 id)은 붙임 · 짝은 id. */
|
* 누가거리만 화면 계산 · 428) 서버 자리로(구간 끝 · 좌표도) · 사용자가 옮긴 · 새로 놓은 관은 그대로 · 서버가 새로
|
||||||
|
* 놓은 관(옛 정본에 없던 id)은 붙임 · 짝은 id. */
|
||||||
export function rebasePipeDraft<P extends FollowingPoint & { id?: string | null }>(
|
export function rebasePipeDraft<P extends FollowingPoint & { id?: string | null }>(
|
||||||
draft: ReadonlyArray<P>,
|
draft: ReadonlyArray<P>,
|
||||||
before: ReadonlyArray<FollowingPoint & { id?: string | null }>,
|
before: ReadonlyArray<FollowingPoint & { id?: string | null }>,
|
||||||
@@ -120,7 +121,14 @@ export function rebasePipeDraft<P extends FollowingPoint & { id?: string | null
|
|||||||
const kept = draft.map((pipe) => {
|
const kept = draft.map((pipe) => {
|
||||||
const was = pipe.id ? old.get(pipe.id) : undefined;
|
const was = pipe.id ? old.get(pipe.id) : undefined;
|
||||||
const now = pipe.id ? moved.get(pipe.id) : undefined;
|
const now = pipe.id ? moved.get(pipe.id) : undefined;
|
||||||
if (!was || !now || Math.abs(pipe.chainage_m - was.chainage_m) > SAME_STATION_M) return pipe;
|
const followed =
|
||||||
|
pipe.x != null &&
|
||||||
|
pipe.y != null &&
|
||||||
|
was?.x != null &&
|
||||||
|
was.y != null &&
|
||||||
|
Math.hypot(pipe.x - was.x, pipe.y - was.y) <= FOLLOW_SAME_XY_M;
|
||||||
|
if (!was || !now || (!followed && Math.abs(pipe.chainage_m - was.chainage_m) > SAME_STATION_M))
|
||||||
|
return pipe;
|
||||||
const shift = now.chainage_m - pipe.chainage_m;
|
const shift = now.chainage_m - pipe.chainage_m;
|
||||||
return {
|
return {
|
||||||
...pipe,
|
...pipe,
|
||||||
@@ -134,6 +142,9 @@ export function rebasePipeDraft<P extends FollowingPoint & { id?: string | null
|
|||||||
return [...kept, ...added].sort((a, b) => a.chainage_m - b.chainage_m);
|
return [...kept, ...added].sort((a, b) => a.chainage_m - b.chainage_m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 따라가기만 한 관 = 정본과 같은 땅 위 자리(m) — 서버 좌표가 mm 3자리라 그 반올림 폭. */
|
||||||
|
const FOLLOW_SAME_XY_M = 0.001;
|
||||||
|
|
||||||
export interface FollowingPoint {
|
export interface FollowingPoint {
|
||||||
chainage_m: number;
|
chainage_m: number;
|
||||||
x?: number | null;
|
x?: number | null;
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/* PLAN 428 헬퍼 — 옮긴 관(0.5 m 넘게)의 횡단: 측점 갈아끼움(기본 설계 + 만진 칸) → 저장 노선 다시 셈(미교차
|
||||||
|
* 넓힘) → 관 길이. 서버 길(기본 설계 · 넓힘)과 같은 값인지 시험이 견줌 · 관 초안 얹기(rebasePipeDraft).
|
||||||
|
* node helper_428_pipe_move.mjs <묶음.mjs> <job.json> <grid.bin>
|
||||||
|
* 짝 = `test_428_pipe_move_mirror.py` · 표준출력 JSON 한 벌.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
const store = () => {
|
||||||
|
const m = new Map();
|
||||||
|
return {
|
||||||
|
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
||||||
|
setItem: (k, v) => m.set(k, String(v)),
|
||||||
|
removeItem: (k) => m.delete(k),
|
||||||
|
key: (i) => [...m.keys()][i] ?? null,
|
||||||
|
get length() {
|
||||||
|
return m.size;
|
||||||
|
},
|
||||||
|
clear: () => m.clear(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
globalThis.window = globalThis;
|
||||||
|
globalThis.sessionStorage = store();
|
||||||
|
globalThis.localStorage = store();
|
||||||
|
const any = new Proxy(function () {}, {
|
||||||
|
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
|
||||||
|
apply: () => any,
|
||||||
|
construct: () => any,
|
||||||
|
});
|
||||||
|
globalThis.document = any;
|
||||||
|
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||||
|
|
||||||
|
const job = JSON.parse(readFileSync(process.argv[3], "utf8"));
|
||||||
|
const gridBytes = readFileSync(process.argv[4]);
|
||||||
|
// 설계 지형 격자 창구만 가짜 — 그 밖 요청은 404(관 초안 · 저장 관 없음)
|
||||||
|
globalThis.fetch = async (url) => {
|
||||||
|
const href = String(url);
|
||||||
|
if (href.includes("/route/terrain-sources"))
|
||||||
|
return Response.json({ design: "las", grid: { model_id: 1, smooth: false, signature: "s" } });
|
||||||
|
if (href.includes("/grid?")) return new Response(gridBytes);
|
||||||
|
return new Response("{}", { status: 404 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const m = await import(pathToFileURL(process.argv[2]).href);
|
||||||
|
const P = "p428";
|
||||||
|
const R = 7;
|
||||||
|
m.writeState("std-cross", job.standard, P);
|
||||||
|
m.rememberRockBoundaryDefault(P, job.rockDefault);
|
||||||
|
|
||||||
|
/** 서버 횡단(표본 · 틀) → 묶음 측점 꼴(지반 = 중심 → 바깥 · 좌 +). */
|
||||||
|
const stationOf = (key, section) => {
|
||||||
|
const samples = section.samples;
|
||||||
|
const side = (sign) =>
|
||||||
|
samples
|
||||||
|
.filter((s) => s.offset_m * sign > 1e-9)
|
||||||
|
.map((s) => ({ offsetM: Math.abs(s.offset_m), z: s.elevation_m }))
|
||||||
|
.sort((a, b) => a.offsetM - b.offsetM);
|
||||||
|
const center = samples.find((s) => Math.abs(s.offset_m) < 1e-9);
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
kind: "pipe",
|
||||||
|
chainageM: section.chainage_m,
|
||||||
|
x: section.center_x,
|
||||||
|
y: section.center_y,
|
||||||
|
left: section.frame.left_xy,
|
||||||
|
azimuthDeg: section.azimuth_deg ?? 0,
|
||||||
|
planRadiusM: null,
|
||||||
|
curveOuterSide: null,
|
||||||
|
curveWideningM: null,
|
||||||
|
ground: { left: side(1), right: side(-1), centerZ: center ? center.elevation_m : null },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const culvertAt = (chainage) => ({ ...job.culvert, chainage_m: chainage });
|
||||||
|
const alignmentAt = (chainage, z) => ({
|
||||||
|
stations: [{ chainage_m: chainage, plan_elevation_m: z }],
|
||||||
|
samples: [
|
||||||
|
{ chainage_m: 0, elevation_m: z },
|
||||||
|
{ chainage_m: 100000, elevation_m: z },
|
||||||
|
],
|
||||||
|
balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 },
|
||||||
|
});
|
||||||
|
const pipeLength = (section) =>
|
||||||
|
m.computeStoredLayouts(section, [section])?.culvert?.pipe?.lengthM ?? null;
|
||||||
|
const pick = (design) => ({
|
||||||
|
section_mode: design.section_mode,
|
||||||
|
ground_type: design.ground_type,
|
||||||
|
ditch_side: design.ditch_side ?? null,
|
||||||
|
two_stage_slope: design.two_stage_slope ?? null,
|
||||||
|
rock_boundary_offset_m: design.rock_boundary_offset_m ?? null,
|
||||||
|
inlet_structure: design.inlet_structure ?? null,
|
||||||
|
cut: design.cut_area_m2 ?? null,
|
||||||
|
fill: design.fill_area_m2 ?? null,
|
||||||
|
ends: design.design_line
|
||||||
|
? [design.design_line[0].offset_m, design.design_line.at(-1).offset_m]
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = {};
|
||||||
|
const oldDetail = () => ({
|
||||||
|
longitudinal: { length_m: 1000, stations: [], profile_alignment: { policy: {} } },
|
||||||
|
cross_sections: [
|
||||||
|
{ ...structuredClone(job.old), kind: "irregular", culvert: culvertAt(job.old.chainage_m) },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 옮긴 관 — 0.5 m 넘게: 갈아끼움 → 다시 셈(넓힘) → 관 길이 */
|
||||||
|
{
|
||||||
|
const swap = m.swapCrossSections(
|
||||||
|
oldDetail(),
|
||||||
|
[stationOf("pipe:p-a", job.moved)],
|
||||||
|
["pipe:p-a"],
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
const section = swap.detail.cross_sections[0];
|
||||||
|
out.swapped = { uphill: section.uphill_side, ...pick(section.design) };
|
||||||
|
section.culvert = culvertAt(job.moved.chainage_m); // 그림 자료가 새 자리에 다시 붙음(withDraftShapes 몫)
|
||||||
|
// 격자는 받기가 끝나야 씀 — 첫 다시 셈이 받기를 띄움 · 받은 뒤 다시 셈이 넓힘
|
||||||
|
await m.refreshCrossDesigns({
|
||||||
|
projectId: P,
|
||||||
|
routeId: R,
|
||||||
|
detail: swap.detail,
|
||||||
|
edits: { station_offsets: {}, curve_radii: {} },
|
||||||
|
alignment: alignmentAt(job.moved.chainage_m, job.elevation),
|
||||||
|
});
|
||||||
|
await new Promise((next) => setTimeout(next, 50));
|
||||||
|
out.firstSamples = section.samples.length; // 격자 받기 전 = 안 넓힘(지문에 격자 유무 · 받은 뒤 다시 셈)
|
||||||
|
await m.refreshCrossDesigns({
|
||||||
|
projectId: P,
|
||||||
|
routeId: R,
|
||||||
|
detail: swap.detail,
|
||||||
|
edits: { station_offsets: {}, curve_radii: {} },
|
||||||
|
alignment: alignmentAt(job.moved.chainage_m, job.elevation),
|
||||||
|
});
|
||||||
|
out.browser = {
|
||||||
|
...pick(section.design),
|
||||||
|
samples: section.samples.length,
|
||||||
|
sampleEnds: [section.samples[0].offset_m, section.samples.at(-1).offset_m],
|
||||||
|
pipeLength: pipeLength(section),
|
||||||
|
};
|
||||||
|
// 서버 = 같은 표본 · 기본 설계 → 서버 Node 가 같은 함수로 구조물 폐회로 면적 · 관 길이(`_recompute`)
|
||||||
|
const server = {
|
||||||
|
...structuredClone(job.moved),
|
||||||
|
kind: "irregular",
|
||||||
|
samples: job.serverSamples,
|
||||||
|
design: { ...job.serverDesign, inlet_structure: job.old.design.inlet_structure },
|
||||||
|
culvert: culvertAt(job.moved.chainage_m),
|
||||||
|
};
|
||||||
|
m.applyStructureAreaRows([server], m.structureAreaRows([server]));
|
||||||
|
out.server = { ...pick(server.design), pipeLength: pipeLength(server) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0.5 m 안 옮김 — 앞 설계 그대로(서버가 행을 옮겨 사용자 설계 값 그대로) */
|
||||||
|
{
|
||||||
|
const near = { ...stationOf("pipe:p-a", job.old), chainageM: job.old.chainage_m + 0.3 };
|
||||||
|
const swap = m.swapCrossSections(oldDetail(), [near], ["pipe:p-a"], 20);
|
||||||
|
out.near = pick(swap.detail.cross_sections[0].design);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 관 초안 얹기 — 따라가기만 한 관(땅 위 자리 = 정본) = 서버 누가거리 · 사용자가 옮긴 관 = 그대로 */
|
||||||
|
out.rebase = m
|
||||||
|
.rebasePipeDraft(
|
||||||
|
[
|
||||||
|
{ id: "f", chainage_m: 105, x: 10, y: 20, source: "user" },
|
||||||
|
{ id: "u", chainage_m: 210, x: 55, y: 66, source: "user" },
|
||||||
|
{ id: "s", chainage_m: 300, source: "user" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ id: "f", chainage_m: 100, x: 10, y: 20 },
|
||||||
|
{ id: "u", chainage_m: 200, x: 50, y: 60 },
|
||||||
|
{ id: "s", chainage_m: 300 },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ id: "f", chainage_m: 104, x: 10.5, y: 20.5, source: "user" },
|
||||||
|
{ id: "u", chainage_m: 203, x: 51, y: 61, source: "user" },
|
||||||
|
{ id: "s", chainage_m: 301, x: 1, y: 1, source: "user" },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map((pipe) => [pipe.id, pipe.chainage_m]);
|
||||||
|
|
||||||
|
process.stdout.write(JSON.stringify(out));
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* PLAN 428 시험 묶음 진입점 — 옮긴 관 측점 갈아끼움 · 저장 노선 다시 셈(넓힘) · 관 길이 · 관 초안 얹기(진짜 모듈 · vite ssr). */
|
||||||
|
export { swapCrossSections } from "../../B06_Section/logic/B06_Section_Station_Swap";
|
||||||
|
export { refreshCrossDesigns } from "../../B06_Section/B06_Section_Cross_Refresh";
|
||||||
|
export {
|
||||||
|
applyStructureAreaRows,
|
||||||
|
computeStoredLayouts,
|
||||||
|
structureAreaRows,
|
||||||
|
} from "../../B06_Section/B06_Section_Structure_Layouts";
|
||||||
|
export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel";
|
||||||
|
export { writeState } from "../../A00_Common/b_page_state";
|
||||||
|
export { rebasePipeDraft } from "../../common_util/common_util_route_follow";
|
||||||
@@ -48,7 +48,11 @@ def test_암선_기본은_config(r):
|
|||||||
def test_M5_관_길이는_공용_캐시(r):
|
def test_M5_관_길이는_공용_캐시(r):
|
||||||
assert r["m5"] == {"before": 8, "cleared": None, "after": 11}
|
assert r["m5"] == {"before": 8, "cleared": None, "after": 11}
|
||||||
page = _read("A00_Common/design/design_page.ts")
|
page = _read("A00_Common/design/design_page.ts")
|
||||||
assert "peekSectionDetail(projectId, pageRouteId(projectId))?.cross_sections" in page
|
# 노선 초안 동안 = 계산 줄 새 노선 상세 · 그 밖 = 공용 캐시(428)
|
||||||
|
assert (
|
||||||
|
"(pipelineDetail(projectId) ?? peekSectionDetail(projectId, pageRouteId(projectId)))"
|
||||||
|
in page
|
||||||
|
)
|
||||||
found = subprocess.run( # noqa: S603, S607 — 고정 실행 파일
|
found = subprocess.run( # noqa: S603, S607 — 고정 실행 파일
|
||||||
["git", "grep", "-l", "setPipeSectionsSource(", "--", "*.ts"],
|
["git", "grep", "-l", "setPipeSectionsSource(", "--", "*.ts"],
|
||||||
cwd=ROOT,
|
cwd=ROOT,
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
"""PLAN 428 — 평면 · 관 옮김 뒤 횡단 형상 · 측점 한 값.
|
||||||
|
|
||||||
|
요구 255-1 · 257-1 · 261 · 분석 424 1장 · 7장.
|
||||||
|
|
||||||
|
못박는 것
|
||||||
|
- 0.5 m 넘게 옮긴 관 측점 = 기본 설계(새 상단측 깎기 · 리핑암 · 표준 측구)
|
||||||
|
+ 사용자가 만진 칸 — 서버가 그 행을 지우고 기본 설계를 세우는 것과 같음 ·
|
||||||
|
0.5 m 안이면 앞 설계 그대로.
|
||||||
|
- 저장 노선 다시 셈(`refreshCrossDesigns`)이 미교차 측점을 서버 `extend_unclosed`
|
||||||
|
와 같이 넓힘 → 브라우저 관 길이 = 서버 관 길이(같은 기하 함수 · 표본 · 설계).
|
||||||
|
- 관 초안 얹기 — 따라가기만 한 관(땅 위 자리 = 정본 x · y) = 서버 누가거리 ·
|
||||||
|
사용자가 옮긴 관 = 그대로.
|
||||||
|
- 관 PUT 끝 서버가 측점을 다시 세움 · GET 관 지점에 x · y.
|
||||||
|
짝: `helper_428_pipe_move.mjs` · 진입점 `helper_428_pipe_move_entry.ts` ·
|
||||||
|
지형 · 노선 틀 = `test_346_2_pipeline_cross_page.py`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
ROOT = HERE.parents[1]
|
||||||
|
for path in (ROOT, HERE):
|
||||||
|
if str(path) not in sys.path:
|
||||||
|
sys.path.insert(0, str(path))
|
||||||
|
|
||||||
|
from test_346_2_pipeline_cross_page import _fake, _line, _sections # noqa: E402
|
||||||
|
|
||||||
|
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||||
|
from B06_Section.B06_Section_Engine_SampleExtend import extend_unclosed # noqa: E402
|
||||||
|
from common_util.common_util_surface_sampler import DtmGridSampler, dtm_grid_bytes # noqa: E402
|
||||||
|
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M # noqa: E402
|
||||||
|
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||||
|
reason="프론트엔드 의존성 미설치",
|
||||||
|
)
|
||||||
|
|
||||||
|
CULVERT = {
|
||||||
|
"type": "pipe",
|
||||||
|
"pipe_kind": "흄관",
|
||||||
|
"diameter_m": 1.0,
|
||||||
|
"min_cover_m": 0.5,
|
||||||
|
"inlet": {"role": "inlet", "structure": "기슭막이", "revet_form": "돌쌓기(찰)"},
|
||||||
|
"outlet": {"role": "outlet", "structure": "기슭막이", "revet_form": "돌쌓기(찰)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _pipe_section(line, sampler, chainage: float) -> dict:
|
||||||
|
data = _sections(line, sampler, [{"id": "p-a", "chainage_m": chainage, "source": "user"}])
|
||||||
|
return next(s for s in data["cross_sections"] if abs(s["chainage_m"] - chainage) < 1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def _server(section: dict, elevation: float, sampler):
|
||||||
|
"""서버 길 — 옮긴 관 측점 = 기본 설계(상단측 깎기 · `compute_default_designs`)
|
||||||
|
→ [저장] 보정 맨 앞 미교차 넓힘(`extend_unclosed_sections` — 저장 설계의
|
||||||
|
단면 유형 · 측구 칸을 입력으로 다시 셈)."""
|
||||||
|
base = {
|
||||||
|
"ground_type": "ripping_rock",
|
||||||
|
"ditch_type": "standard",
|
||||||
|
"standard": STANDARD_CROSS_SECTION,
|
||||||
|
"rock_boundary_offset_m": STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||||
|
}
|
||||||
|
stored = compute_cross_design(
|
||||||
|
section["samples"],
|
||||||
|
elevation,
|
||||||
|
section_mode=f"{section.get('uphill_side') or 'left'}_cut",
|
||||||
|
**base,
|
||||||
|
)
|
||||||
|
|
||||||
|
def compute(samples):
|
||||||
|
return compute_cross_design(
|
||||||
|
samples,
|
||||||
|
elevation,
|
||||||
|
section_mode=stored["section_mode"],
|
||||||
|
ditch_side=stored.get("ditch_side"),
|
||||||
|
ditch_enabled=stored.get("ditch_enabled"),
|
||||||
|
ditch_choice=stored.get("ditch_choice"),
|
||||||
|
two_stage_slope=bool(stored.get("two_stage_slope", False)),
|
||||||
|
**base,
|
||||||
|
)
|
||||||
|
|
||||||
|
wide = extend_unclosed(section["samples"], section["frame"], compute, sampler)
|
||||||
|
if not (wide and (wide[1]["left_m"] or wide[1]["right_m"])):
|
||||||
|
return section["samples"], stored, wide
|
||||||
|
return wide[0], compute(wide[0]), wide
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def r(tmp_path_factory) -> dict:
|
||||||
|
work = tmp_path_factory.mktemp("b428")
|
||||||
|
build = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||||
|
[
|
||||||
|
"node",
|
||||||
|
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||||
|
"build",
|
||||||
|
"--configLoader",
|
||||||
|
"runner",
|
||||||
|
"--ssr",
|
||||||
|
"../resources/tester/helper_428_pipe_move_entry.ts",
|
||||||
|
"--outDir",
|
||||||
|
str(work),
|
||||||
|
"--logLevel",
|
||||||
|
"error",
|
||||||
|
],
|
||||||
|
cwd=ROOT,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
assert build.returncode == 0, build.stderr
|
||||||
|
bundle = work / "bundle.mjs"
|
||||||
|
shutil.copy(work / "helper_428_pipe_move_entry.js", bundle)
|
||||||
|
with tempfile.TemporaryDirectory() as folder:
|
||||||
|
npz, nodes = _fake(Path(folder))
|
||||||
|
line = _line(nodes)
|
||||||
|
sampler = DtmGridSampler.from_npz(npz)
|
||||||
|
old = _pipe_section(line, sampler, 90.0)
|
||||||
|
moved = _pipe_section(line, sampler, 96.0) # 6 m 옮김 — 서버가 옛 행을 지우고 새로 세움
|
||||||
|
# 표본 20 m 밖까지 닿는 성토 높이 — 넓힘이 실제로 도는 자리
|
||||||
|
for rise in (10.0, 14.0, 18.0, 24.0):
|
||||||
|
elevation = float(moved["center_z"]) + rise
|
||||||
|
samples, design, wide = _server(moved, elevation, sampler)
|
||||||
|
if samples is not moved["samples"]:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
pytest.fail("넓힘이 도는 계획고를 못 찾음")
|
||||||
|
uphill = moved.get("uphill_side") or "left"
|
||||||
|
other = "right" if uphill == "left" else "left"
|
||||||
|
old["design"] = {
|
||||||
|
"design_elevation_m": elevation,
|
||||||
|
"ground_type": "soil",
|
||||||
|
"section_mode": f"{other}_cut",
|
||||||
|
"ditch_side": other,
|
||||||
|
"two_stage_slope": True,
|
||||||
|
"rock_boundary_offset_m": 9.0,
|
||||||
|
"inlet_structure": "revet",
|
||||||
|
}
|
||||||
|
job = {
|
||||||
|
"old": old,
|
||||||
|
"moved": moved,
|
||||||
|
"elevation": elevation,
|
||||||
|
"standard": STANDARD_CROSS_SECTION,
|
||||||
|
"rockDefault": STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||||
|
"culvert": CULVERT,
|
||||||
|
"serverSamples": samples,
|
||||||
|
"serverDesign": design,
|
||||||
|
}
|
||||||
|
path = Path(folder) / "job.json"
|
||||||
|
path.write_text(json.dumps(job), encoding="utf-8")
|
||||||
|
grid = Path(folder) / "grid.bin"
|
||||||
|
grid.write_bytes(dtm_grid_bytes(npz))
|
||||||
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||||
|
["node", str(HERE / "helper_428_pipe_move.mjs"), str(bundle), str(path), str(grid)],
|
||||||
|
cwd=ROOT,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
out = json.loads(proc.stdout)
|
||||||
|
out["job"] = {"uphill": uphill, "other": other, "wide": wide[1], "samples": len(samples)}
|
||||||
|
out["serverDesign"] = design
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def test_옮긴_관_측점_기본_설계와_만진_칸(r):
|
||||||
|
s = r["swapped"]
|
||||||
|
assert s["uphill"] == r["job"]["uphill"] # 상단측 = 새 지반
|
||||||
|
assert s["section_mode"] == f"{r['job']['uphill']}_cut"
|
||||||
|
assert s["ground_type"] == "ripping_rock"
|
||||||
|
assert s["ditch_side"] is None and s["two_stage_slope"] is None
|
||||||
|
assert s["rock_boundary_offset_m"] is None
|
||||||
|
assert s["inlet_structure"] == "revet" # 사용자가 만진 칸은 따라감
|
||||||
|
|
||||||
|
|
||||||
|
def test_0_5m_안_옮김_앞_설계_그대로(r):
|
||||||
|
n = r["near"]
|
||||||
|
assert n["section_mode"] == f"{r['job']['other']}_cut"
|
||||||
|
assert n["ground_type"] == "soil" and n["ditch_side"] == r["job"]["other"]
|
||||||
|
assert n["rock_boundary_offset_m"] == 9.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_저장_노선_다시_셈이_서버처럼_넓힘(r):
|
||||||
|
b = r["browser"]
|
||||||
|
s = r["server"] # 구조물 폐회로 면적까지(서버 Node 와 같은 함수)
|
||||||
|
assert r["job"]["wide"]["left_m"] or r["job"]["wide"]["right_m"]
|
||||||
|
assert r["firstSamples"] < b["samples"] == r["job"]["samples"] # 격자 받은 뒤 넓힘
|
||||||
|
assert abs(b["cut"] - s["cut"]) < 1e-3 and abs(b["fill"] - s["fill"]) < 1e-3
|
||||||
|
d = r["serverDesign"]
|
||||||
|
ends = [d["design_line"][0]["offset_m"], d["design_line"][-1]["offset_m"]]
|
||||||
|
assert all(abs(a - e) < 1e-3 for a, e in zip(b["ends"], ends))
|
||||||
|
|
||||||
|
|
||||||
|
def test_옮긴_관_관_길이_브라우저_대_서버(r):
|
||||||
|
got = r["browser"]["pipeLength"]
|
||||||
|
want = r["server"]["pipeLength"]
|
||||||
|
assert got is not None and want is not None and got > 0
|
||||||
|
assert abs(got - want) < 1e-3, (got, want)
|
||||||
|
|
||||||
|
|
||||||
|
def test_관_초안_얹기_따라가기만_한_관은_서버_누가거리(r):
|
||||||
|
assert r["rebase"] == [["f", 104], ["u", 210], ["s", 301]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_관_PUT_끝_측점_다시_세움_GET_좌표():
|
||||||
|
from B04_PreProcess import B04_PreProcess_Router_Basins as basins
|
||||||
|
|
||||||
|
put = inspect.getsource(basins.put_pipe_points)
|
||||||
|
assert (
|
||||||
|
put.index("save_pipe_points") < put.index("_stand_pipe_stations(") < put.index("_payload(")
|
||||||
|
)
|
||||||
|
stand = inspect.getsource(basins._stand_pipe_stations)
|
||||||
|
assert "ensure_structure_stations(project_id)" in stand and "except Exception" in stand
|
||||||
|
raw = inspect.getsource(basins._raw_payload)
|
||||||
|
assert '"x": round(float(pipe.x), 3)' in raw and '"y": round(float(pipe.y), 3)' in raw
|
||||||
Reference in New Issue
Block a user