Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -10,8 +10,8 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 가운데 = 위 칸(보기마다 다름) + 아래 종단도 한 벌. 종단도는 두 보기 모두 위 칸 아래 흐름에
|
||||
선다(345-3 — 종단 보기 위 칸 = 평면 노선 편집 · 3D 는 모달) · 높이 끌기는 그대로. */
|
||||
/* 가운데 = 위 칸(보기마다 다름) + 아래 종단도 한 벌. 종단 보기에서 종단도는 평면(노선 편집) 위에
|
||||
떠 있는 판(absolute · 평면은 전체 영역 · 345-5)이고, 횡단 보기에서는 카드 칸 아래 흐름에 선다. */
|
||||
.design-page__main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -35,7 +35,7 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.design-page__main > .design-page__profile > .b05-route-profile {
|
||||
.design-page__main[data-view="cross"] > .design-page__profile > .b05-route-profile {
|
||||
position: relative;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
* GET /projects/{id}/route/plan → 예상노선·계획노선 정점(사업지 좌표계 m)
|
||||
* POST /projects/{id}/route/replan → 고친 계획노선으로 갈아 끼우고 재계산
|
||||
* POST /projects/{id}/route/replan/reset → 계획노선을 예상노선으로 되돌리고 재계산
|
||||
* POST /projects/{id}/route/profile-preview → 지금 노드의 균형 종단(편집 창 종단 띠)
|
||||
* GET /projects/{id}/route/terrain-sources → 고를 수 있는 분석 지형(라이다 · 수치지형도)
|
||||
*
|
||||
* 편집 창 분석(지반고 · 횡단 미리보기 · 종단 띠 · 추천)은 **고른 분석 지형**(`terrain`)을 실어
|
||||
* 편집 창 분석(지반고 · 횡단 미리보기 · 추천)은 **고른 분석 지형**(`terrain`)을 실어
|
||||
* 보낸다(PLAN 133-1) — [확인] 재계산은 확정 지표면 그대로라 안 실음.
|
||||
*
|
||||
* 재계산은 배수유역부터 전 단계를 다시 도는 무거운 작업이라(용화 67측점 기준 3분대)
|
||||
@@ -260,21 +259,3 @@ export async function resetRoutePlan(projectId: string): Promise<RouteReplanResp
|
||||
REPLAN_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
/** 지금 노드의 **균형 종단**(PLAN 75-1) — [확인]과 같은 폴리라인 · 같은 셈. 파일은 안 쓴다.
|
||||
* 답의 `profile` 은 추천 창과 같은 꼴(`parseBalance` 로 편다). */
|
||||
export async function fetchProfilePreview(
|
||||
projectId: string,
|
||||
vertices: RouteReplanVertex[],
|
||||
criteria: unknown,
|
||||
): Promise<unknown> {
|
||||
const payload = await requestJson<{ profile: unknown }>(
|
||||
`/projects/${projectId}/route/profile-preview`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ vertices, criteria, terrain: analysisTerrain(projectId) }),
|
||||
},
|
||||
120000,
|
||||
);
|
||||
return payload.profile;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,10 @@ import functools
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from B05_Profile import B05_Profile_Router_Replan as replan
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial import read_initial_choice
|
||||
@@ -277,19 +276,12 @@ async def balanced_profile_of(
|
||||
points: list[dict[str, float]],
|
||||
criteria: Any,
|
||||
design_curves: list[dict[str, Any]] | None = None,
|
||||
terrain: Literal["las", "sheet"] | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""노선 정점의 균형 종단 한 벌(`route_profile`) — [확인] 체인 · 편집 창 종단 띠가 함께 씀.
|
||||
|
||||
`terrain` 은 편집 창 종단 띠만 바꿈(PLAN 133-1) — None = 지금 설계 지형([확인] 체인).
|
||||
"""
|
||||
"""노선 정점의 균형 종단 한 벌(`route_profile`) — [확인] 체인 몫(지금 설계 지형)."""
|
||||
from B05_Profile.B05_Profile_Engine_Grade import legal_grade_limit_pct
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import route_profile
|
||||
from B05_Profile.B05_Profile_Engine_Solver import _MODELS_SUBDIR
|
||||
from common_util.common_util_surface_confirmation import (
|
||||
analysis_terrain_params,
|
||||
get_surface_confirmation_params,
|
||||
)
|
||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||
from common_util.common_util_surface_sampler import build_surface_sampler
|
||||
|
||||
grade_class, design_speed, terrain_type = await replan._road_settings(project_id)
|
||||
@@ -299,9 +291,8 @@ async def balanced_profile_of(
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
selection = await get_surface_confirmation_params(connection, str(project_id))
|
||||
selection = analysis_terrain_params(selection, terrain)
|
||||
try:
|
||||
# 측점 생성과 같은 1 m 쌍선형 표본(추천 점수의 2 m 비용면 아님) — 띠 값 = [확인] 뒤 값
|
||||
# 측점 생성과 같은 1 m 쌍선형 표본(추천 점수의 2 m 비용면 아님)
|
||||
surface = await asyncio.to_thread(
|
||||
build_surface_sampler,
|
||||
project_root / _MODELS_SUBDIR,
|
||||
@@ -358,47 +349,3 @@ async def refresh_initial_snapshot(
|
||||
logger.info("노선 [확인] 초기값 다시 뜸: project_id=%s route_id=%s", project_id, route_id)
|
||||
except Exception: # noqa: BLE001 — 계산 결과는 그대로 남는다
|
||||
logger.exception("노선 [확인] 초기값 스냅샷 실패: project_id=%s", project_id)
|
||||
|
||||
|
||||
class ProfilePreviewRequest(BaseModel):
|
||||
"""편집 창 종단 띠(PLAN 75-1) — 지금 노드 · 지금 추천 제약."""
|
||||
|
||||
vertices: list[replan.RouteVertexInput] = Field(..., min_length=2)
|
||||
criteria: replan.RouteInitialCriteria | None = None
|
||||
terrain: Literal["las", "sheet"] | None = None
|
||||
|
||||
|
||||
@replan.router.post("/{project_id}/route/profile-preview", response_model=None)
|
||||
async def profile_preview(
|
||||
project_id: UUID, request: ProfilePreviewRequest
|
||||
) -> dict[str, Any] | JSONResponse:
|
||||
"""지금 노드의 균형 종단 — [확인]과 같은 폴리라인(같은 R · CSV 자릿수) · 같은 셈.
|
||||
|
||||
파일 · DB 를 쓰지 않는다. 「종단 반영」 켬일 때 편집 창 띠가 노드를 놓을 때마다 부른다.
|
||||
"""
|
||||
from common_util.common_util_route_polyline import build_planned_polyline
|
||||
|
||||
paths = await replan._project_paths(project_id)
|
||||
if paths is None:
|
||||
return JSONResponse(status_code=404, content=replan._PROJECT_PATH_MISSING)
|
||||
project_root, _ = paths
|
||||
radius_m, _, _ = await replan._plan_criteria(project_id)
|
||||
built = build_planned_polyline(
|
||||
[(v.x, v.y) for v in request.vertices],
|
||||
min_radius_m=radius_m,
|
||||
simplify=False,
|
||||
curve_flags=[bool(v.curve) for v in request.vertices],
|
||||
radii=[v.radius_m for v in request.vertices],
|
||||
)
|
||||
# [확인]은 CSV(소수 4자리)로 적은 뒤 읽는다 — 같은 점을 셈하게 같은 자릿수로.
|
||||
points = [{"x": round(float(x), 4), "y": round(float(y), 4)} for x, y in built.vertices]
|
||||
curves = [curve.as_dict() for curve in built.curves]
|
||||
profile = await balanced_profile_of(
|
||||
project_id, project_root, points, request.criteria, curves, request.terrain
|
||||
)
|
||||
if profile is None:
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"status": "error", "message": "이 노선으로 균형 종단을 세우지 못했습니다."},
|
||||
)
|
||||
return {"status": "success", "project_id": str(project_id), "profile": profile}
|
||||
|
||||
@@ -68,7 +68,6 @@ import {
|
||||
writeRouteLocks,
|
||||
} from "./B05_Profile_UI_RouteEdit_Lock";
|
||||
import { createInitialPanel, type InitialPanelParams } from "./B05_Profile_UI_RouteEdit_Initial";
|
||||
import { createProfileStrip } from "./B05_Profile_UI_RouteEdit_ProfileStrip";
|
||||
import {
|
||||
applyArcLocks,
|
||||
applyCurveLimits,
|
||||
@@ -277,22 +276,6 @@ export async function mountRouteEditor(
|
||||
planMinRadiusM: () => minRadiusM,
|
||||
apply: (snapshot, message) => restoreSnapshot(snapshot, message, true),
|
||||
});
|
||||
/** 지도 아래 종단 띠 — 노드를 놓을 때마다 다시 묻는다(PLAN 75-1). 셈은 `_ProfileStrip` 몫. */
|
||||
const strip = createProfileStrip({
|
||||
root: overlay.querySelector<HTMLElement>("[data-strip]")!,
|
||||
projectId,
|
||||
withProfile: initial.withProfile,
|
||||
now: () => ({
|
||||
vertices: planned.map(([x, y], index) => ({
|
||||
x,
|
||||
y,
|
||||
curve: curveOn[index] !== false,
|
||||
radius_m: curveRadius[index] ?? null,
|
||||
})),
|
||||
line: plannedLine.length ? plannedLine : planned,
|
||||
criteria: initial.current().criteria,
|
||||
}),
|
||||
});
|
||||
// [반영 전으로] — 마지막 반영(페이지를 세운 때) 노선 · 그 걸음의 「현재 추천」으로. 되돌리기로 다시 살림.
|
||||
overlay.querySelector('[data-act="cancel"]')!.addEventListener("click", () => {
|
||||
const snapshot = history?.reset();
|
||||
@@ -463,7 +446,6 @@ export async function mountRouteEditor(
|
||||
draw();
|
||||
if (record) history?.commit(snapshotNow());
|
||||
historyControls.sync();
|
||||
strip.refresh();
|
||||
}
|
||||
|
||||
/** 지금 편집값을 사진 한 벌로 담는다 — 되돌리기가 쌓아 두는 것. */
|
||||
@@ -544,7 +526,6 @@ export async function mountRouteEditor(
|
||||
commit: () => {
|
||||
history?.commit(snapshotNow());
|
||||
historyControls.sync();
|
||||
strip.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -594,7 +575,7 @@ export async function mountRouteEditor(
|
||||
terrain: pickedTerrain,
|
||||
stationSnap: () => stationSnap,
|
||||
initial,
|
||||
profileShown: strip.settle,
|
||||
profileShown: async () => initial.withProfile(), // 띠 없음(345-5) — 지금 추천 갈래가 정함
|
||||
onApplied,
|
||||
});
|
||||
|
||||
@@ -682,7 +663,6 @@ export async function mountRouteEditor(
|
||||
useBackground(background);
|
||||
measure.clear();
|
||||
slope.refresh();
|
||||
strip.refresh();
|
||||
void crossPreview.refresh();
|
||||
showStatus();
|
||||
draw();
|
||||
@@ -692,7 +672,6 @@ export async function mountRouteEditor(
|
||||
resize(); // 칸 크기가 서 있으면 여기서 보던 화면을 연다(아니면 처음 보일 때).
|
||||
showStatus();
|
||||
draw();
|
||||
strip.refresh();
|
||||
} catch (error) {
|
||||
status.textContent = error instanceof Error ? error.message : "노선을 읽지 못했습니다.";
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function createRouteEditChrome(
|
||||
<span data-slot="tools"></span>
|
||||
<i class="b05-routeedit__divider" aria-hidden="true"></i>
|
||||
<label class="b05-routeedit__terrain" hidden
|
||||
title="편집 창 분석(등고선 · 지반고 · 기울기 · 횡단 미리보기 · 종단 띠 · 추천)에 쓸 지형 — 확정 지표면 · 종횡단은 안 바뀝니다">분석 지형
|
||||
title="편집 창 분석(등고선 · 지반고 · 기울기 · 횡단 미리보기 · 추천)에 쓸 지형 — 확정 지표면 · 종횡단은 안 바뀝니다">분석 지형
|
||||
<select class="ui-select ui-select--sm" data-act="terrain">
|
||||
<option value="las">라이다</option>
|
||||
<option value="sheet">수치지형도</option>
|
||||
@@ -112,12 +112,6 @@ export function createRouteEditChrome(
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b05-routeedit__strip" data-strip>
|
||||
<div class="b05-routeedit__strip-head">
|
||||
<span data-strip-note>종단을 읽는 중…</span>
|
||||
</div>
|
||||
<div class="b05-routeedit__strip-graph" data-strip-graph></div>
|
||||
</div>
|
||||
<div class="b05-routeedit__busy" hidden><span></span></div>
|
||||
</div>
|
||||
<aside class="b05-routeedit__side-wrap">
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_ProfileStrip.ts
|
||||
* 계획노선 편집 창 지도 아래 **종단 띠**(PLAN 75-1) — 지금 노선 전체 연장의 원지반선 · 계획선 ·
|
||||
* 측점 눈금 한 줄.
|
||||
*
|
||||
* 보여 주기만 한다(PLAN 77-5) — 종단 여부는 지금 추천 갈래가 정한다(`withProfile`). 종단 갈래 =
|
||||
* 계획선은 지금 노드로 서버가 다시 낸 균형 종단(`/route/profile-preview` — [확인] 체인과 같은 셈).
|
||||
* 예상노선 추적 = 지반선만(계획고 = 지반고 · `/route/elevations`). [확인]도 같은 규칙을 탄다
|
||||
* (`_Apply` 의 `balanced_profile`).
|
||||
*
|
||||
* 갱신은 노드를 놓을 때마다(끌기 중 아님) · 최신 요청만 채택(seq) · 기다리는 동안 옛 그림은 흐리게.
|
||||
* 계산 자리 = 서버 단독 — 여기는 받은 값을 그리기만 한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
analysisTerrain,
|
||||
fetchProfilePreview,
|
||||
fetchRouteElevations,
|
||||
type RouteReplanVertex,
|
||||
} from "./B05_Profile_Api_Replan";
|
||||
import { parseBalance, profileSvg } from "./B05_Profile_UI_RouteEdit_Recommend_Profile";
|
||||
import { lengthOf, pointsAt, stationChainages } from "./B05_Profile_UI_RouteEdit_Slope";
|
||||
|
||||
type Vertex = [number, number];
|
||||
|
||||
/** 지반선만 그릴 때 측점 간격(m) — 균형 종단(서버 `STATION_M`)과 같음. */
|
||||
const STATION_M = 20;
|
||||
|
||||
export interface ProfileStripParams {
|
||||
root: HTMLElement;
|
||||
projectId: string;
|
||||
/** 지금 추천 갈래가 종단을 반영하는가(예상노선 추적만 아님). */
|
||||
withProfile: () => boolean;
|
||||
/** 지금 편집값 — 노드(서버로) · 그려진 노선(지반선 측점) · 지금 추천 제약. */
|
||||
now: () => { vertices: RouteReplanVertex[]; line: ReadonlyArray<Vertex>; criteria: unknown };
|
||||
}
|
||||
|
||||
export interface ProfileStrip {
|
||||
/** 노선이 바뀌었다(놓음 · 되돌리기 · 추천 적용) — 다시 묻는다. */
|
||||
refresh: () => void;
|
||||
/** [확인] 직전 — 지금 노선으로 띠를 맞추고 응답을 기다려, 띠에 보인 계획선이 균형 종단인지
|
||||
* 돌려준다(보인 것 = 적용된 것). 띠가 못 그렸으면 갈래 값. */
|
||||
settle: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function createProfileStrip(params: ProfileStripParams): ProfileStrip {
|
||||
const { root, projectId } = params;
|
||||
const note = root.querySelector<HTMLElement>("[data-strip-note]")!;
|
||||
const graph = root.querySelector<HTMLElement>("[data-strip-graph]")!;
|
||||
let seq = 0;
|
||||
/** 마지막으로 그렸거나 묻는 중인 값 — 같은 노선 · 같은 갈래 종단이면 다시 묻지 않는다. */
|
||||
let lastKey = "";
|
||||
/** 마지막 요청(끝날 때까지) · 띠에 그려진 계획선이 균형 종단인가(못 그렸으면 null). */
|
||||
let pending: Promise<void> = Promise.resolve();
|
||||
let shownOn: boolean | null = null;
|
||||
|
||||
async function groundOnly(line: ReadonlyArray<Vertex>) {
|
||||
const stationsM = stationChainages(lengthOf(line), STATION_M);
|
||||
const z = await fetchRouteElevations(projectId, pointsAt(line, stationsM));
|
||||
const known = stationsM.flatMap((s, i) => (z[i] === null ? [] : [[s, z[i]!] as const]));
|
||||
if (known.length < 2) throw new Error("노선 위 지반고를 읽지 못했습니다.");
|
||||
return {
|
||||
stationsM: known.map(([s]) => s),
|
||||
groundM: known.map(([, h]) => h),
|
||||
planM: [] as number[],
|
||||
};
|
||||
}
|
||||
|
||||
function refresh(): void {
|
||||
const on = params.withProfile();
|
||||
const { vertices, line, criteria } = params.now();
|
||||
const key = JSON.stringify([on, vertices, criteria, analysisTerrain(projectId)]);
|
||||
if (vertices.length < 2 || line.length < 2 || key === lastKey) return;
|
||||
lastKey = key;
|
||||
pending = draw(on, vertices, line, criteria);
|
||||
}
|
||||
|
||||
async function draw(
|
||||
on: boolean,
|
||||
vertices: RouteReplanVertex[],
|
||||
line: ReadonlyArray<Vertex>,
|
||||
criteria: unknown,
|
||||
): Promise<void> {
|
||||
const ticket = ++seq;
|
||||
root.classList.add("is-waiting");
|
||||
note.textContent = "종단을 읽는 중…";
|
||||
try {
|
||||
const profile = on
|
||||
? parseBalance(await fetchProfilePreview(projectId, vertices, criteria))
|
||||
: await groundOnly(line);
|
||||
if (ticket !== seq || !root.isConnected) return; // 그 사이 노선이 또 바뀌었다.
|
||||
if (!profile) throw new Error("균형 종단을 세우지 못했습니다.");
|
||||
const total = profile.stationsM[profile.stationsM.length - 1];
|
||||
graph.innerHTML = profileSvg(profile, true);
|
||||
shownOn = on;
|
||||
note.textContent = `${on ? "원지반선 · 계획 종단선" : "원지반선(계획고 = 지반고)"} · 연장 ${total.toFixed(1)}m`;
|
||||
} catch (error) {
|
||||
if (ticket !== seq) return;
|
||||
lastKey = ""; // 실패 — 다음 번에 다시 묻는다.
|
||||
shownOn = null;
|
||||
graph.innerHTML = "";
|
||||
note.textContent = error instanceof Error ? error.message : "종단을 읽지 못했습니다.";
|
||||
} finally {
|
||||
if (ticket === seq) root.classList.remove("is-waiting");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
refresh,
|
||||
settle: async () => {
|
||||
refresh();
|
||||
await pending;
|
||||
return shownOn ?? params.withProfile();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -85,11 +85,9 @@ const PROFILE_H = 96;
|
||||
const PAD = 6;
|
||||
|
||||
/** 종단 그림(SVG) — 지반선 · 균형 계획선. 가로 = 측점 · 세로 = 표고(창에 맞춰 늘림).
|
||||
* `ticks` 면 아래 가장자리에 측점 눈금(100 m 마다 길게) — 편집 창 종단 띠(PLAN 75-1).
|
||||
* 계획선이 비면(종단 반영 끔) 지반선만. */
|
||||
export function profileSvg(
|
||||
balance: Pick<BalanceProfile, "stationsM" | "groundM" | "planM"> | null,
|
||||
ticks = false,
|
||||
): string {
|
||||
if (!balance) return "";
|
||||
const { stationsM, groundM, planM } = balance;
|
||||
@@ -104,18 +102,8 @@ export function profileSvg(
|
||||
return `${i ? "L" : "M"}${x.toFixed(1)} ${y.toFixed(1)}`;
|
||||
})
|
||||
.join("");
|
||||
const tickPath = ticks
|
||||
? stationsM
|
||||
.map((s) => {
|
||||
const x = (PAD + ((PROFILE_W - 2 * PAD) * s) / total).toFixed(1);
|
||||
const long = Math.abs(s / 100 - Math.round(s / 100)) < 1e-6;
|
||||
return `M${x} ${PROFILE_H}V${PROFILE_H - (long ? 7 : 3)}`;
|
||||
})
|
||||
.join("")
|
||||
: "";
|
||||
return `<svg viewBox="0 0 ${PROFILE_W} ${PROFILE_H}" preserveAspectRatio="none"
|
||||
role="img" aria-label="균형 종단 미리 보기 — 지반선과 계획선">
|
||||
${ticks ? `<path class="is-tick" d="${tickPath}" vector-effect="non-scaling-stroke" />` : ""}
|
||||
<path class="is-ground" d="${path(groundM)}" vector-effect="non-scaling-stroke" />
|
||||
<path class="is-plan" d="${planM.length ? path(planM) : ""}" vector-effect="non-scaling-stroke" /></svg>`;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
}
|
||||
|
||||
/* 하단 종단 패널: 그래프 + 12행 도면 테이블을 담기 위해 화면 높이의 60%를 차지한다.
|
||||
페이지 안에서는 위 칸(평면 · 횡단 카드) 아래 **흐름**에 선다(`design_page.css` · 345-3) —
|
||||
여기 absolute 는 페이지 밖 쓰임 꼴. 접기 핸들로 내리면 위 칸이 전체 영역을 갖는다. */
|
||||
평면(노선 편집)을 밀어내지 않고 그 위를 덮는 오버레이로 띄운다(345-5) — 판을 여닫아도 평면
|
||||
크기가 안 바뀜 · 가려진 아래쪽은 팬으로 봄. 횡단 보기에서는 카드 칸 아래 흐름(`design_page.css`). */
|
||||
.b05-route-profile {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
|
||||
@@ -399,59 +399,3 @@
|
||||
background: var(--color-primary, #7c3aed);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 지도 아래 종단 띠(PLAN 75-1) — 원지반선 · 계획선 · 측점 눈금 한 줄. 묻는 동안 옛 그림은 흐리게. */
|
||||
.b05-routeedit__strip {
|
||||
display: flex;
|
||||
flex: none;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4, 4px);
|
||||
padding: var(--spacing-8) var(--spacing-16);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b05-routeedit__strip-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-12);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 그림 칸 높이는 미리 잡아 둠 — 응답이 늦게 와도 띠 높이가 안 바뀌어 지도 크기 셈이 안 어긋남. */
|
||||
.b05-routeedit__strip-graph {
|
||||
flex: none;
|
||||
height: 88px;
|
||||
}
|
||||
|
||||
.b05-routeedit__strip-graph svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 88px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-8, 6px);
|
||||
background: var(--color-surface);
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.b05-routeedit__strip.is-waiting .b05-routeedit__strip-graph svg {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.b05-routeedit__strip-graph .is-ground {
|
||||
fill: none;
|
||||
stroke: var(--color-text-secondary);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.b05-routeedit__strip-graph .is-plan {
|
||||
fill: none;
|
||||
stroke: var(--color-primary);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.b05-routeedit__strip-graph .is-tick {
|
||||
stroke: var(--color-text-secondary);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from B05_Profile import B05_Profile_Router_Replan as replan
|
||||
from B05_Profile import B05_Profile_Router_RouteInitial as initial
|
||||
from B05_Profile import B05_Profile_Router_Terrain as terrain_router
|
||||
from common_util.common_util_surface_confirmation import (
|
||||
SHEET_ANALYSIS_FILTER,
|
||||
@@ -143,8 +142,6 @@ def test_허용_목록_밖_거절(bad):
|
||||
terrain_router.CrossPreviewRequest(
|
||||
vertices=[{"x": 0, "y": 0}, {"x": 1, "y": 1}], chainage_m=0, terrain=bad
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
initial.ProfilePreviewRequest(vertices=[{"x": 0, "y": 0}, {"x": 1, "y": 1}], terrain=bad)
|
||||
with pytest.raises(ValidationError):
|
||||
replan.RouteInitialRequest(terrain=bad)
|
||||
|
||||
@@ -170,8 +167,8 @@ def test_화면_고르기_배선():
|
||||
assert '<option value="sheet">수치지형도</option>' in chrome
|
||||
assert 'sources?.sources.includes("las") && !!sources?.sources.includes("sheet")' in main
|
||||
assert 'saved === "sheet" || saved === "las" ? saved : fallback' in api
|
||||
# 모달 분석 넷이 고른 지형을 실어 보냄 · [확인] 재계산은 안 실음
|
||||
assert api.count("terrain: analysisTerrain(projectId)") == 3
|
||||
# 모달 분석 셋(종단 띠는 345-5 에서 없앰)이 고른 지형을 실어 보냄 · [확인] 재계산은 안 실음
|
||||
assert api.count("terrain: analysisTerrain(projectId)") == 2
|
||||
assert "terrain: analysisTerrain(projectId)" in initial_api
|
||||
replan_fn = api.split("export async function replanRoute")[1].split("export ")[0]
|
||||
assert "terrain" not in replan_fn
|
||||
|
||||
@@ -78,11 +78,14 @@ def test_편집기는_본문_페이지_수명():
|
||||
assert len(view.splitlines()) <= 700
|
||||
|
||||
|
||||
def test_종단도_판은_두_보기_모두_흐름():
|
||||
def test_종단도_판은_횡단_보기만_흐름():
|
||||
"""345-5 — 종단 보기는 다시 뜬 판(평면 위 오버레이) · 횡단 보기만 흐름."""
|
||||
css = _read("A00_Common/design/design_page.css")
|
||||
rule = _block(css, ".design-page__main > .design-page__profile > .b05-route-profile")
|
||||
rule = _block(
|
||||
css, '.design-page__main[data-view="cross"] > .design-page__profile > .b05-route-profile'
|
||||
)
|
||||
assert "position: relative;" in rule and "flex: none;" in rule
|
||||
assert '[data-view="cross"] > .design-page__profile > .b05-route-profile' not in css
|
||||
assert "\n.design-page__main > .design-page__profile > .b05-route-profile" not in css
|
||||
|
||||
|
||||
def test_계획노선_칸_편집_단추_지움():
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""345-5 사용자 의견(요구 233) — 종단 보기 평면 = 가운데 전체 영역 · 아래 종단 판 = 뜬 판 ·
|
||||
노선 편집 안 「원지반선 · 계획 종단선」 띠 없음 · 아래 종단 판 종단 그래프는 그대로.
|
||||
|
||||
꼴은 글로 봄(화면 확인은 브레인).
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_345_5_plan_full_overlay.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _block(css: str, selector: str) -> str:
|
||||
found = re.search(rf"^{re.escape(selector)} \{{([^}}]*)\}}", css, re.M)
|
||||
assert found, selector
|
||||
return found.group(1)
|
||||
|
||||
|
||||
def test_평면은_가운데_전체_영역():
|
||||
page = _read("A00_Common/design/design_page.css")
|
||||
main = _block(page, ".design-page__main")
|
||||
assert "position: relative;" in main and "height: 100%;" in main
|
||||
assert "flex: 1 1 auto;" in _block(page, ".design-page__center")
|
||||
# 종단 보기에서 판을 흐름에 세우는 규칙 없음 — 가운데가 위아래 끝까지
|
||||
flow = re.findall(r"^(\.design-page__main[^{\n]*\.b05-route-profile) \{", page, re.M)
|
||||
assert flow == [
|
||||
'.design-page__main[data-view="cross"] > .design-page__profile > .b05-route-profile'
|
||||
]
|
||||
route = _read("B05_Profile/B05_Profile_UI_Style_RouteEdit.css")
|
||||
assert "flex: 1 1 auto;" in _block(route, ".b05-routeedit")
|
||||
assert "flex: 1 1 auto;" in _block(route, ".b05-routeedit__canvas-wrap")
|
||||
# 캔버스 크기 = 칸 크기(ResizeObserver) · 평면은 가운데 칸에 붙음
|
||||
editor = _read("B05_Profile/B05_Profile_UI_RouteEdit.ts")
|
||||
assert "new ResizeObserver(() => resize())" in editor
|
||||
assert (
|
||||
'sizeWatch.observe(overlay.querySelector<HTMLElement>(".b05-routeedit__canvas-wrap")!)'
|
||||
in editor
|
||||
)
|
||||
view = _read("A00_Common/design/design_view_profile.ts")
|
||||
assert "center.append(mainContent);" in view
|
||||
assert "mountRouteEditor(mainContent, host, activeProjectId, rebuildAfterReplan," in view
|
||||
|
||||
|
||||
def test_종단_판은_평면_위_뜬_판():
|
||||
style = _read("B05_Profile/B05_Profile_UI_Style.css")
|
||||
panel = _block(style, ".b05-route-profile")
|
||||
for line in ("position: absolute;", "z-index: 3;", "bottom: 0;", "left: 0;", "right: 0;"):
|
||||
assert line in panel
|
||||
# 끌어 높이 · 접기 · 새 창 떼기 그대로
|
||||
assert "height: var(--b05-profile-height, 60dvh);" in panel
|
||||
assert ".b05-route-profile.is-collapsed > .ui-resizer" in style
|
||||
page = _read("A00_Common/design/design_page.css")
|
||||
assert "position: relative;" in _block(page, ".b05-route-profile.is-popout")
|
||||
|
||||
|
||||
def test_띠_없음_종단_그래프_있음():
|
||||
chrome = _read("B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts")
|
||||
assert "data-strip" not in chrome
|
||||
assert not (ROOT / "B05_Profile/B05_Profile_UI_RouteEdit_ProfileStrip.ts").exists()
|
||||
# 아래 종단 판(종단 그래프 · 표 · 유토곡선)은 그대로 세움
|
||||
view = _read("A00_Common/design/design_view_profile.ts")
|
||||
assert "const profilePanel = createRouteProfilePanel(" in view
|
||||
assert (ROOT / "B05_Profile/B05_Profile_UI_Profile_Panel.ts").exists()
|
||||
@@ -1,4 +1,4 @@
|
||||
"""편집 창 종단 띠 — [확인]과 같은 노선 점으로 균형 종단을 셈한다(PLAN 75-1).
|
||||
"""편집 창 종단 띠 — 없앰(345-5 · 아래 종단 판 종단 그래프가 그 몫) · [확인] 목표 셈 그대로.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_75_profile_strip.py -q
|
||||
"""
|
||||
@@ -7,90 +7,30 @@ import asyncio
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from B05_Profile import B05_Profile_Router_Replan as replan
|
||||
from B05_Profile import B05_Profile_Router_RouteInitial as initial
|
||||
from common_util.common_util_route_geometry import read_planned_route_csv
|
||||
|
||||
NODES = [(0.0, 0.0), (120.0, 35.0), (180.0, 140.0), (310.0, 150.5)]
|
||||
RADIUS_M = 25.0
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
|
||||
|
||||
def _strip_points(monkeypatch, tmp_path: Path, criteria=None) -> tuple[list, object]:
|
||||
seen: dict = {}
|
||||
|
||||
async def paths(_pid):
|
||||
return tmp_path, "x"
|
||||
|
||||
async def plan_criteria(_pid):
|
||||
return RADIUS_M, 0.0, 0.0
|
||||
|
||||
async def profile_of(_pid, _root, points, crit, curves=None, _terrain="las"):
|
||||
seen["points"], seen["criteria"], seen["curves"] = points, crit, curves
|
||||
return {"stations_m": [0.0, 20.0], "plan_m": [1.0, 2.0]}
|
||||
|
||||
monkeypatch.setattr(replan, "_project_paths", paths)
|
||||
monkeypatch.setattr(replan, "_plan_criteria", plan_criteria)
|
||||
monkeypatch.setattr(initial, "balanced_profile_of", profile_of)
|
||||
request = initial.ProfilePreviewRequest(
|
||||
vertices=[
|
||||
replan.RouteVertexInput(x=x, y=y, curve=i != 2, radius_m=40.0 if i == 1 else None)
|
||||
for i, (x, y) in enumerate(NODES)
|
||||
],
|
||||
criteria=criteria,
|
||||
)
|
||||
answer = asyncio.run(initial.profile_preview(uuid4(), request))
|
||||
assert answer["profile"]["plan_m"] == [1.0, 2.0]
|
||||
return seen["points"], seen["criteria"]
|
||||
def _read(name: str) -> str:
|
||||
return (B05 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_띠_노선점은_확인이_읽는_CSV와_같다(monkeypatch, tmp_path: Path) -> None:
|
||||
points, _ = _strip_points(monkeypatch, tmp_path)
|
||||
working = tmp_path / "planned_route.csv"
|
||||
replan._write_planned_polyline(
|
||||
working,
|
||||
NODES,
|
||||
RADIUS_M,
|
||||
simplify=False,
|
||||
curve_flags=[True, True, False, True],
|
||||
radii=[None, 40.0, None, None],
|
||||
)
|
||||
stored = read_planned_route_csv(working)
|
||||
assert stored is not None
|
||||
assert [(p["x"], p["y"]) for p in points] == [(v.x, v.y) for v in stored.vertices]
|
||||
assert len(points) > len(NODES) # 원호가 펴져 있다
|
||||
|
||||
|
||||
def test_띠는_그_노선의_곡선표로_확폭을_맨다(monkeypatch, tmp_path: Path) -> None:
|
||||
seen: dict = {}
|
||||
|
||||
async def paths(_pid):
|
||||
return tmp_path, "x"
|
||||
|
||||
async def plan_criteria(_pid):
|
||||
return RADIUS_M, 0.0, 0.0
|
||||
|
||||
async def profile_of(_pid, _root, _points, _crit, curves=None, _terrain="las"):
|
||||
seen["curves"] = curves
|
||||
return {"stations_m": [0.0, 20.0], "plan_m": [1.0, 2.0]}
|
||||
|
||||
monkeypatch.setattr(replan, "_project_paths", paths)
|
||||
monkeypatch.setattr(replan, "_plan_criteria", plan_criteria)
|
||||
monkeypatch.setattr(initial, "balanced_profile_of", profile_of)
|
||||
request = initial.ProfilePreviewRequest(
|
||||
vertices=[
|
||||
replan.RouteVertexInput(x=x, y=y, curve=i != 2, radius_m=40.0 if i == 1 else None)
|
||||
for i, (x, y) in enumerate(NODES)
|
||||
]
|
||||
)
|
||||
asyncio.run(initial.profile_preview(uuid4(), request))
|
||||
assert seen["curves"] and 40.0 in [c["radius_m"] for c in seen["curves"]]
|
||||
assert all({"start", "apex", "end", "radius_m"} <= set(c) for c in seen["curves"])
|
||||
|
||||
|
||||
def test_띠는_지금_추천_제약을_싣는다(monkeypatch, tmp_path: Path) -> None:
|
||||
criteria = replan.RouteInitialCriteria(max_grade_pct=12.0)
|
||||
_, got = _strip_points(monkeypatch, tmp_path, criteria)
|
||||
assert got.max_grade_pct == 12.0
|
||||
def test_띠_없음_화면_요청_서버_길():
|
||||
assert not (B05 / "B05_Profile_UI_RouteEdit_ProfileStrip.ts").exists()
|
||||
chrome = _read("B05_Profile_UI_RouteEdit_Chrome.ts")
|
||||
assert "data-strip" not in chrome and "종단 띠" not in chrome
|
||||
assert "routeedit__strip" not in _read("B05_Profile_UI_Style_RouteEdit.css")
|
||||
editor = _read("B05_Profile_UI_RouteEdit.ts")
|
||||
assert "ProfileStrip" not in editor and "strip." not in editor
|
||||
# [노선 반영]의 균형 종단 여부 = 지금 추천 갈래(띠가 보이던 값과 같은 규칙)
|
||||
assert "profileShown: async () => initial.withProfile()," in editor
|
||||
assert "profile-preview" not in _read("B05_Profile_Api_Replan.ts")
|
||||
assert "is-tick" not in _read("B05_Profile_UI_RouteEdit_Recommend_Profile.ts")
|
||||
assert not hasattr(initial, "profile_preview") and not hasattr(initial, "ProfilePreviewRequest")
|
||||
paths = {getattr(r, "path", "") for r in initial.replan.router.routes}
|
||||
assert not any(p.endswith("/route/profile-preview") for p in paths)
|
||||
|
||||
|
||||
def test_확인_목표는_같은_셈의_측점_계획고(monkeypatch, tmp_path: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user