Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createSidePanels, type SidePanels } from "@ui/ui_template_side_panels";
|
||||
import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint";
|
||||
import { bindMarkToggle } from "./B05_Profile_UI_RouteEdit_Marks";
|
||||
|
||||
@@ -59,6 +60,12 @@ export interface RouteEditChrome {
|
||||
terrainSelect: HTMLSelectElement;
|
||||
}
|
||||
|
||||
/** 손잡이 자리(24) + 틈(8) — 접혀도 남는 폭(`.ui-side-panels` margin-left). */
|
||||
const SIDE_HANDLE_PX = 32;
|
||||
|
||||
/** 횡단 판 몸 → 패널 묶음 — 고른 측점 횡단을 띄울 때 접힌 판을 폄(382-2 · `_Cross`). */
|
||||
export const sidePanelsOf = new WeakMap<HTMLElement, SidePanels>();
|
||||
|
||||
// 글판 접기 기억(345-5 덧 3) — 페이지 동안만(모듈 값) · 다시 열어도 그대로.
|
||||
let hintOpen = false;
|
||||
let infoCollapsed = false;
|
||||
@@ -135,26 +142,33 @@ export function createRouteEditChrome(
|
||||
</div>
|
||||
</div>
|
||||
<div class="b05-routeedit__busy" hidden><span></span></div>
|
||||
<aside class="b05-routeedit__side-wrap">
|
||||
<button type="button" class="ui-btn ui-btn--glass b05-routeedit__side-toggle"
|
||||
data-act="side-toggle" aria-expanded="true" title="오른쪽 횡단 미리보기 칸 접기 · 펴기">횡단</button>
|
||||
<div class="b05-routeedit__side"></div>
|
||||
</aside>`;
|
||||
<aside class="b05-routeedit__side-wrap"></aside>`;
|
||||
parent.append(overlay);
|
||||
// 부른 쪽 단추([3D])도 같은 유리 단추로 — 지도 위에 떠 있는 도구줄.
|
||||
tools.forEach((tool) => tool.classList.replace("ui-btn--ghost", "ui-btn--glass"));
|
||||
overlay.querySelector('[data-slot="tools"]')!.replaceWith(...tools);
|
||||
|
||||
// 횡단 미리보기 = 접는 칸 — 좁은 화면은 접힌 채 시작(지도가 먼저다).
|
||||
const sideWrap = overlay.querySelector<HTMLElement>(".b05-routeedit__side-wrap")!;
|
||||
const sideToggle = overlay.querySelector<HTMLButtonElement>('[data-act="side-toggle"]')!;
|
||||
const setSide = (open: boolean): void => {
|
||||
sideWrap.classList.toggle("is-collapsed", !open);
|
||||
overlay.classList.toggle("is-side-open", open); // 회전 단추 · 잰 값 창이 판을 비킴
|
||||
sideToggle.setAttribute("aria-expanded", String(open));
|
||||
};
|
||||
setSide(window.innerWidth >= 1500);
|
||||
sideToggle.addEventListener("click", () => setSide(sideWrap.classList.contains("is-collapsed")));
|
||||
// 횡단 미리보기 = 패널 양식(382-1 · 손잡이 · 머리 · 폭 끌기 · 열림 · 폭 기억) · 자리는 지도 위 덮개 그대로 ·
|
||||
// 기억이 없으면 좁은 화면은 접힌 채 시작(지도가 먼저다) · 잰 값 창이 판을 비킴(--b05-side-open).
|
||||
const side = document.createElement("div");
|
||||
side.className = "b05-routeedit__side";
|
||||
const setSideOpen = (): void =>
|
||||
overlay.style.setProperty(
|
||||
"--b05-side-open",
|
||||
`${(panels.current() ? panels.width() : 0) + SIDE_HANDLE_PX}px`,
|
||||
);
|
||||
const panels = createSidePanels({
|
||||
prefKey: "b05-routeedit-cross",
|
||||
panels: [{ id: "cross", title: "횡단", body: side }],
|
||||
defaultOpen: window.innerWidth >= 1500 ? "cross" : null,
|
||||
defaultWidth: 460,
|
||||
follow: false,
|
||||
onChange: setSideOpen,
|
||||
onResize: setSideOpen,
|
||||
});
|
||||
sidePanelsOf.set(side, panels);
|
||||
overlay.querySelector(".b05-routeedit__side-wrap")!.append(panels.root);
|
||||
setSideOpen();
|
||||
|
||||
// 도움말 = 기본 접힘 · [?] 한 번에 펼침(345-5 덧 3).
|
||||
const hint = overlay.querySelector<HTMLElement>(".b05-routeedit__hint")!;
|
||||
|
||||
@@ -12,12 +12,27 @@
|
||||
* 값 = 계산 묶음 그 측점(365-3) — 측점 · 지반 표본 = ground 단계 · 계획고 · 설계선 = 종단 → cross 단계
|
||||
* (B06 카드 · 새 노선 상세와 같은 함수 `stationSection`) · 노선 · 계획고가 바뀔 때마다(cross 차례) 다시 그림 ·
|
||||
* 서버 왕복 없음. 성토사면 길이는 B06 화면이 쓰는 `fillSlopeLengths`.
|
||||
*
|
||||
* 고름 연동(382-2) — 공용 고름 한 칸(`design_selection`) 구독: 일반측점 고름 = 그 측점 횡단 · 평면 법선 ·
|
||||
* 구조물 고름 = 평면 그 자리 마름모만 · 평면 눈금 클릭 = 공용 고름에 적음(`from` b05-plan · 제 알림 거름).
|
||||
* 보는 측점 = 측점 열쇠(grid:k) — 묶음 차례마다 열쇠로 다시 짚음(노선을 고쳐도 같은 측점).
|
||||
* 파란 법선 폭 = 그 측점 발자국 기록(가게 `footprint.records`) 비탈 끝 좌 · 우 · 깎기 여부(색).
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
import type { CrossPreviewResponse } from "./B05_Profile_Api_Replan";
|
||||
import { pagePipeline } from "../A00_Common/design/design_pipeline_page";
|
||||
import type { DesignPipeline } from "../A00_Common/design/logic/design_pipeline";
|
||||
import type {
|
||||
PipelineStation,
|
||||
PlanFootprint,
|
||||
} from "../A00_Common/design/logic/design_pipeline_types";
|
||||
import {
|
||||
nearestWithin,
|
||||
selectDesign,
|
||||
subscribeSelection,
|
||||
type DesignSelection,
|
||||
} from "../A00_Common/design/design_selection";
|
||||
import { useDesignStore } from "../A00_Common/design/design_store";
|
||||
import { stationSection } from "./logic/B05_Profile_Pipeline_Detail";
|
||||
import {
|
||||
@@ -27,6 +42,8 @@ import {
|
||||
} from "./B05_Profile_UI_RouteEdit_Cross_Draw";
|
||||
import { readByKey, stateKey } from "../A00_Common/b_page_state";
|
||||
import { stationLabel as formatStation } from "../common_util/common_util_station";
|
||||
import { sidePanelsOf } from "./B05_Profile_UI_RouteEdit_Chrome";
|
||||
import type { FootprintRecord } from "./logic/B05_Profile_PlanFootprint";
|
||||
|
||||
export interface CrossPreviewParams {
|
||||
projectId: string;
|
||||
@@ -43,19 +60,31 @@ export interface CrossPreviewParams {
|
||||
onShown?: () => void;
|
||||
}
|
||||
|
||||
/** 위 판에 떠 있는 측점과 그 좌·우 횡단 반폭(m) — 평면도가 반폭 선을 그리는 재료. */
|
||||
/** 고른 측점과 그 좌·우 폭(m · 좌 + 방향 거리) — 평면도가 법선 · 마름모를 그리는 재료. */
|
||||
export interface CrossWatch {
|
||||
chainageM: number;
|
||||
leftM: number;
|
||||
rightM: number;
|
||||
/** 비탈 끝 깎기(절토) 여부 — 발자국 기록이 없으면 없음(옛 파랑 · 반폭). */
|
||||
leftCut?: boolean;
|
||||
rightCut?: boolean;
|
||||
/** 구조물 · 관 자리 — 법선 없이 마름모만. */
|
||||
structure?: true;
|
||||
}
|
||||
|
||||
/** 공용 고름에 적는 이름 — 평면 눈금 클릭(제 알림은 거름). */
|
||||
export const PLAN_PICK_FROM = "b05-plan";
|
||||
|
||||
/** 법선을 긋는 측점 갈래 — 일반측점(시점 · 종점 포함). */
|
||||
const isGridStation = (station: PipelineStation): boolean =>
|
||||
station.kind === "grid" || station.kind === "end";
|
||||
|
||||
export interface CrossPreviewWindow {
|
||||
/** 그 측점의 횡단을 위 판에 낸다. 아래 판은 비운다 — 전후 비교는 노선을 고쳤을 때만. */
|
||||
open: (chainageM: number) => Promise<void>;
|
||||
/** 다시 그림(묶음 지금 값) — 노선이 바뀌었으면 전후로. 보던 것이 없으면 아무 일도 없다. */
|
||||
refresh: () => Promise<void>;
|
||||
/** 위 판에 떠 있는 측점과 반폭. 떠 있지 않으면 null. */
|
||||
/** 고른 측점과 폭(발자국 비탈 끝 · 없으면 반폭) · 구조물이면 마름모. 고른 것 없으면 null. */
|
||||
watched: () => CrossWatch | null;
|
||||
}
|
||||
|
||||
@@ -193,18 +222,33 @@ export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWind
|
||||
params.side.append(current.root, previous.root);
|
||||
const interval = (): number => params.request().station_interval_m;
|
||||
|
||||
/** 지금 보고 있는 측점(누가거리). 아직 없으면 null. */
|
||||
let watching: number | null = null;
|
||||
/** 지금 보는 측점 — 열쇠(묶음 전 null) · 누가거리(열쇠를 못 찾을 때) · 구조물 고름(마름모만). */
|
||||
let watching: { key: string | null; chainageM: number; structure: boolean } | null = null;
|
||||
/** 위 판에 그려 둔 것 — 다음 번에 아래로 내릴 재료 · 그때 노선 초안(바뀌면 전후로). */
|
||||
let shown: CrossPreviewResponse | null = null;
|
||||
let drawnRoute: unknown = null;
|
||||
|
||||
const stationsNow = (): PipelineStation[] => pipe?.output("ground")?.stations ?? [];
|
||||
/** 열쇠로 다시 짚은 지금 측점 — 열쇠가 없으면 들고 있던 누가거리 근처. */
|
||||
function located(): PipelineStation | null {
|
||||
if (!watching) return null;
|
||||
const list = stationsNow();
|
||||
const key = watching.key;
|
||||
const at = watching.chainageM;
|
||||
return (
|
||||
(key ? list.find((station) => station.key === key) : undefined) ??
|
||||
nearestWithin(list, at, (station) => station.chainageM) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function redraw(): void {
|
||||
if (watching === null) return;
|
||||
if (watching === null || watching.structure) return;
|
||||
const route = pipe?.input("route") ?? null;
|
||||
if (route !== drawnRoute && shown) previous.show(shown, interval());
|
||||
drawnRoute = route;
|
||||
shown = pipelineCrossAt(pipe, watching, interval(), appliedHalfWidth(params.projectId));
|
||||
const at = located()?.chainageM ?? watching.chainageM;
|
||||
shown = pipelineCrossAt(pipe, at, interval(), appliedHalfWidth(params.projectId));
|
||||
if (shown) current.show(shown, interval());
|
||||
else current.wait("계산 중…");
|
||||
params.onShown?.();
|
||||
@@ -230,33 +274,98 @@ export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWind
|
||||
const stop = (): void => {
|
||||
stopStore();
|
||||
stopPipe();
|
||||
stopPick();
|
||||
};
|
||||
|
||||
/** 그 측점 횡단을 위 판에 · 아래 판은 비움(전후 비교는 노선을 고쳤을 때만) · 접힌 판은 폄. */
|
||||
function showStation(key: string | null, chainageM: number): void {
|
||||
// 아래 판은 **노선을 고쳤을 때만** 채운다(2026-09-12 사용자 지시 ⑤). 예전에는 같은
|
||||
// 측점을 두 번 누르면 위·아래에 **같은 그림 두 장**이 떠 전후 비교가 되지 않았다.
|
||||
previous.show(null, interval());
|
||||
watching = { key, chainageM, structure: false };
|
||||
drawnRoute = pipe?.input("route") ?? null;
|
||||
shown = null;
|
||||
redraw();
|
||||
const panels = sidePanelsOf.get(params.side);
|
||||
if (panels && panels.current() === null) panels.open("cross");
|
||||
}
|
||||
|
||||
/** 다른 보기(종단 · 3D · 목록 · 횡단 카드) 고름 → 일반측점이면 그 측점 횡단 · 구조물이면 마름모. */
|
||||
function follow(selection: DesignSelection): void {
|
||||
attach();
|
||||
const at = selection.chainageM;
|
||||
if (at === null) {
|
||||
watching = null;
|
||||
shown = null;
|
||||
current.show(null, interval());
|
||||
previous.show(null, interval());
|
||||
params.onShown?.();
|
||||
return;
|
||||
}
|
||||
const station = nearestWithin(stationsNow(), at, (entry) => entry.chainageM);
|
||||
if (!selection.structureId && (!station || isGridStation(station)))
|
||||
return showStation(station?.key ?? null, station?.chainageM ?? at);
|
||||
watching = { key: station?.key ?? null, chainageM: station?.chainageM ?? at, structure: true };
|
||||
params.onShown?.();
|
||||
}
|
||||
const stopPick = subscribeSelection(params.projectId, (selection, from) => {
|
||||
if (from !== PLAN_PICK_FROM) follow(selection);
|
||||
});
|
||||
attach();
|
||||
|
||||
/** 그 측점 발자국 기록(열쇠 · 없으면 누가거리) — 발자국 단계 전이면 없음. */
|
||||
const footprintAt = (key: string | null, chainageM: number): FootprintRecord | undefined => {
|
||||
const footprint = useDesignStore(params.projectId, null).get("footprint") as
|
||||
PlanFootprint | null | undefined;
|
||||
const records = (footprint?.records ?? []) as FootprintRecord[];
|
||||
return (
|
||||
(key ? records.find((record) => record.key === key) : undefined) ??
|
||||
nearestWithin(records, chainageM, (record) => record.chainage_m)
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
async open(chainageM) {
|
||||
// 아래 판은 **노선을 고쳤을 때만** 채운다(2026-09-12 사용자 지시 ⑤). 예전에는 같은
|
||||
// 측점을 두 번 누르면 위·아래에 **같은 그림 두 장**이 떠 전후 비교가 되지 않았다.
|
||||
previous.show(null, interval());
|
||||
watching = chainageM;
|
||||
attach();
|
||||
drawnRoute = pipe?.input("route") ?? null;
|
||||
shown = null;
|
||||
redraw();
|
||||
const list = stationsNow();
|
||||
const station = list.length
|
||||
? list.reduce((best, next) =>
|
||||
Math.abs(next.chainageM - chainageM) < Math.abs(best.chainageM - chainageM)
|
||||
? next
|
||||
: best,
|
||||
)
|
||||
: null;
|
||||
const at = station?.chainageM ?? chainageM;
|
||||
showStation(station?.key ?? null, at);
|
||||
selectDesign(
|
||||
params.projectId,
|
||||
{ chainageM: at, structureId: null },
|
||||
{ from: PLAN_PICK_FROM },
|
||||
);
|
||||
},
|
||||
async refresh() {
|
||||
attach();
|
||||
redraw();
|
||||
},
|
||||
// 자리는 보던 측점 그대로 — 노선을 끄는 동안 평면도가 그 누가거리로 다시 짚는다.
|
||||
// 반폭 = 적용해 둔 「횡단 반폭」 · 없으면 그린 원지반 선 범위 — 좌·우 같음.
|
||||
watched: () =>
|
||||
watching === null || !shown
|
||||
? null
|
||||
: {
|
||||
chainageM: watching,
|
||||
leftM: shown.cross_half_width_m,
|
||||
rightM: shown.cross_half_width_m,
|
||||
},
|
||||
// 자리 = 열쇠로 다시 짚은 측점 — 노선을 끄는 동안 평면도가 그 누가거리로 다시 짚는다.
|
||||
// 폭 = 그 측점 발자국 비탈 끝 좌 · 우 · 기록이 없으면 「횡단 반폭」(없으면 원지반 선 범위) 양쪽.
|
||||
watched: () => {
|
||||
if (!watching) return null;
|
||||
const station = located();
|
||||
const chainageM = station?.chainageM ?? watching.chainageM;
|
||||
if (watching.structure || (station && !isGridStation(station)))
|
||||
return { chainageM, leftM: 0, rightM: 0, structure: true };
|
||||
const record = footprintAt(station?.key ?? watching.key, chainageM);
|
||||
if (record)
|
||||
return {
|
||||
chainageM,
|
||||
leftM: record.left_end_m,
|
||||
rightM: -record.right_end_m,
|
||||
leftCut: record.left_cut,
|
||||
rightCut: record.right_cut,
|
||||
};
|
||||
if (!shown) return null;
|
||||
return { chainageM, leftM: shown.cross_half_width_m, rightM: shown.cross_half_width_m };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -393,8 +393,9 @@ function drawMeasureMarks(
|
||||
}
|
||||
|
||||
/**
|
||||
* 횡단이 떠 있는 측점의 **좌·우 반폭 선** — 측점에서 노선에 직각, 길이 = 우측 패널 「횡단 반폭」
|
||||
* (평면 거리 · 횡단 미리보기의 원지반 선 범위와 같다).
|
||||
* 고른 측점의 **좌·우 법선**(382-2) — 측점에서 노선에 직각, 길이 = 그 측점 발자국 비탈 끝 좌 · 우 ·
|
||||
* 색 = 깎기(절토) · 쌓기(성토) 토큰(`_Footprint` 와 같은 것) · 기록이 없으면 「횡단 반폭」 양쪽 파랑 ·
|
||||
* 측점 자리에 고리(눈금 강조). 구조물 · 관 고름 = 그 자리 마름모(배수유역도 `markStation` 꼴)만.
|
||||
*
|
||||
* 자리·방향은 **지금 그려진 노선**으로 매 프레임 다시 짚는다(`stationFrameAt` — 서버
|
||||
* `generate_sections` 짝) — 노드를 끄는 동안 선이 네트워크 없이 바로 따라간다.
|
||||
@@ -410,17 +411,37 @@ function drawCrossWidth(
|
||||
if (!frame) return;
|
||||
const [ox, oy] = frame.origin;
|
||||
const [lx, ly] = frame.left;
|
||||
const ends: Vertex[] = [
|
||||
[ox + lx * watch.leftM, oy + ly * watch.leftM],
|
||||
[ox - lx * watch.rightM, oy - ly * watch.rightM],
|
||||
];
|
||||
const [cx, cy] = scene.toScreen(frame.origin);
|
||||
context.save();
|
||||
context.strokeStyle = CROSS_WIDTH_COLOR;
|
||||
context.fillStyle = CROSS_WIDTH_COLOR;
|
||||
context.setLineDash([]);
|
||||
if (watch.structure) {
|
||||
context.translate(cx, cy);
|
||||
context.rotate(Math.PI / 4);
|
||||
context.beginPath();
|
||||
context.rect(-7, -7, 14, 14);
|
||||
context.fillStyle = "rgba(255, 196, 0, 0.35)";
|
||||
context.fill();
|
||||
context.lineWidth = 2.5;
|
||||
context.strokeStyle = "#ff9800";
|
||||
context.stroke();
|
||||
context.restore();
|
||||
return;
|
||||
}
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const tone = (cut: boolean | undefined): string =>
|
||||
cut === undefined
|
||||
? CROSS_WIDTH_COLOR
|
||||
: style.getPropertyValue(cut ? "--b05-footprint-cut" : "--b05-footprint-fill").trim() ||
|
||||
(cut ? "#dc2626" : "#2563eb");
|
||||
const ends: Array<[Vertex, string]> = [
|
||||
[[ox + lx * watch.leftM, oy + ly * watch.leftM], tone(watch.leftCut)],
|
||||
[[ox - lx * watch.rightM, oy - ly * watch.rightM], tone(watch.rightCut)],
|
||||
];
|
||||
context.lineWidth = 2.4;
|
||||
ends.forEach((end) => {
|
||||
ends.forEach(([end, color]) => {
|
||||
const [x, y] = scene.toScreen(end);
|
||||
context.strokeStyle = color;
|
||||
context.fillStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(cx, cy);
|
||||
context.lineTo(x, y);
|
||||
@@ -429,6 +450,11 @@ function drawCrossWidth(
|
||||
context.arc(x, y, 3, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
});
|
||||
context.strokeStyle = CROSS_WIDTH_COLOR;
|
||||
context.lineWidth = 2;
|
||||
context.beginPath();
|
||||
context.arc(cx, cy, 6, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,17 +4,13 @@
|
||||
/* 평면 = 가운데 칸 전체(352-1) — 머리 줄 · 테두리 없음. 도구줄은 3D 와 같은 떠 있는 유리 단추
|
||||
(`.b05-route__view-controls` · ☰ 비킴 `--wf-burger-pad`) · 횡단 판은 오른쪽을 덮는 판. */
|
||||
.b05-routeedit {
|
||||
--b05-side-open: 0px; /* 횡단 판이 펴져 지도를 덮는 폭 */
|
||||
--b05-side-open: 0px; /* 횡단 판이 지도를 덮는 폭(손잡이 포함 · `_Chrome` 이 적음) */
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.b05-routeedit.is-side-open {
|
||||
--b05-side-open: 460px;
|
||||
}
|
||||
|
||||
/* 도구줄 — 3D 도구줄 규격 그대로 · 회전 단추도 이 줄(366-2) · 넘치면 줄바꿈. */
|
||||
.b05-routeedit__tools {
|
||||
right: var(--spacing-12);
|
||||
@@ -187,9 +183,8 @@
|
||||
border-top: 2px solid var(--map-route, #f97316);
|
||||
}
|
||||
|
||||
/* 오른쪽 덮는 판 — 위아래 반씩 나눠 **지금 횡단**과 **이전 횡단**이 앉는다(352-1). 지도 위에 얹고
|
||||
아래 종단 판(z 3)이 그 위를 덮는다. 접는 칸(345-3) — 좁은 화면은 접힌 채 시작(`_Chrome`) ·
|
||||
[횡단] 단추로 펴고 접는다. 빈 자리는 지도 조작을 통과. */
|
||||
/* 오른쪽 덮는 판 — 패널 양식(382-1 · `ui_template_side_panels` · 손잡이 · 머리 · 폭 끌기) 을 지도 위에 얹음 ·
|
||||
위아래 반씩 **지금 횡단** · **이전 횡단**(352-1) · 아래 종단 판(z 3)이 그 위를 덮음 · 빈 자리는 지도 조작을 통과. */
|
||||
.b05-routeedit__side-wrap {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@@ -197,24 +192,20 @@
|
||||
right: var(--spacing-12);
|
||||
bottom: var(--spacing-12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.b05-routeedit__side-wrap.is-collapsed {
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.b05-routeedit__side-wrap > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.b05-routeedit__side-toggle {
|
||||
align-self: flex-end;
|
||||
min-width: 56px;
|
||||
height: 34px;
|
||||
.b05-routeedit__side-wrap .ui-side-panels__panel {
|
||||
box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
.b05-routeedit__side-wrap .ui-side-panels__body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.b05-routeedit__side {
|
||||
@@ -222,14 +213,10 @@
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
width: 452px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.b05-routeedit__side-wrap.is-collapsed > .b05-routeedit__side {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b05-routeedit__cross {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
@@ -241,7 +228,6 @@
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-16, 12px);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
.b05-routeedit__cross-head {
|
||||
|
||||
@@ -180,6 +180,7 @@ const store = load("A00_Common", "design", "design_store.ts");
|
||||
// B05 오른쪽 횡단 판(365-3) — 그림 조각만 가짜 · 값 고르기는 진짜
|
||||
STUBS["b_svg_zoom_pan"] = { attachSvgZoomPan: () => undefined };
|
||||
STUBS["B05_Profile_UI_RouteEdit_Cross_Draw"] = { CROSS_PLOT_PAD: {}, summarizeCross: () => "" };
|
||||
STUBS["B05_Profile_UI_RouteEdit_Chrome"] = { sidePanelsOf: new WeakMap() }; // 판 양식(382-1) — 화면 밖
|
||||
const pane = load("B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts");
|
||||
const r3 = (value) => (typeof value === "number" ? Math.round(value * 1000) / 1000 : null);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* 계획노선 편집 창 **측점 반폭 선 길이**(계획서 22-1) 단위검증 헬퍼 —
|
||||
/* 계획노선 편집 창 **측점 법선 · 고름 연동**(계획서 22-1 · 382-2) 단위검증 헬퍼 —
|
||||
* `B05_Profile_UI_RouteEdit_Cross.ts` 를 트랜스파일해 Node 로 돌린다.
|
||||
* 계산 묶음 · 그림 조각은 가짜로 채운다 — 「무슨 반폭으로 선을 잡나」만 본다(365-3 · 서버 길 없음 ·
|
||||
* 묶음 그 측점 원지반 표본 = 좌우 20 m).
|
||||
* 계산 묶음 · 가게 · 그림 조각은 가짜로 채운다 — 공용 고름(`design_selection`)은 진짜.
|
||||
* 묶음 측점 = 일반측점 grid:2(40) · grid:3(60) · grid:5(100) · 관 pipe:a(80) · 원지반 표본 좌우 20 m.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -57,6 +57,19 @@ globalThis.ResizeObserver = class {
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
const grid = (k, chainageM) => ({ key: `grid:${k}`, kind: "grid", chainageM });
|
||||
let stations = [
|
||||
grid(2, 40),
|
||||
grid(3, 60),
|
||||
{ key: "pipe:a", kind: "pipe", chainageM: 80 },
|
||||
grid(5, 100),
|
||||
];
|
||||
let footprint = null;
|
||||
let route = { v: 1 };
|
||||
// 접힌 판 펴기 — 몸(side)에 묶인 패널 묶음 흉내
|
||||
const panels = { now: null, current: () => panels.now, open: (id) => (panels.now = id) };
|
||||
const side = { append() {} };
|
||||
|
||||
const STUBS = {
|
||||
"../A00_Common/b_svg_zoom_pan": { attachSvgZoomPan: () => {} },
|
||||
"../common_util/common_util_station": { stationLabel: (m) => String(m) },
|
||||
@@ -65,17 +78,19 @@ const STUBS = {
|
||||
CROSS_PLOT_PAD: 0,
|
||||
summarizeCross: () => "",
|
||||
},
|
||||
// 계산 묶음 흉내 — 측점 하나(100 m) · 원지반 표본 좌우 20 m
|
||||
"./B05_Profile_UI_RouteEdit_Chrome": { sidePanelsOf: new WeakMap([[side, panels]]) },
|
||||
"../A00_Common/design/design_pipeline_page": {
|
||||
pagePipeline: () => ({
|
||||
output: (name) =>
|
||||
name === "ground" ? { stations: [{ key: "grid:5", chainageM: 100 }] } : null,
|
||||
input: () => null,
|
||||
output: (name) => (name === "ground" ? { stations } : null),
|
||||
input: () => route,
|
||||
subscribe: () => () => undefined,
|
||||
}),
|
||||
},
|
||||
"../A00_Common/design/design_store": {
|
||||
useDesignStore: () => ({ subscribe: () => () => undefined }),
|
||||
useDesignStore: () => ({
|
||||
subscribe: () => () => undefined,
|
||||
get: (name) => (name === "footprint" ? footprint : null),
|
||||
}),
|
||||
},
|
||||
"./logic/B05_Profile_Pipeline_Detail": {
|
||||
stationSection: (station) => ({
|
||||
@@ -108,12 +123,26 @@ function loadTs(file) {
|
||||
const { createCrossPreview } = loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts"),
|
||||
);
|
||||
const pick = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"));
|
||||
|
||||
const record = (key, chainage_m, left, right, leftCut, rightCut) => ({
|
||||
key,
|
||||
chainage_m,
|
||||
left_edge_m: 2.5,
|
||||
left_end_m: left,
|
||||
right_edge_m: -2.5,
|
||||
right_end_m: right,
|
||||
left_cut: leftCut,
|
||||
right_cut: rightCut,
|
||||
});
|
||||
|
||||
async function run() {
|
||||
let shownCount = 0;
|
||||
const cross = createCrossPreview({
|
||||
projectId: "p-1",
|
||||
side: { append() {} },
|
||||
side,
|
||||
request: () => ({ vertices: [], min_radius_m: 12, station_interval_m: 20 }),
|
||||
onShown: () => (shownCount += 1),
|
||||
});
|
||||
const out = {};
|
||||
out.before = cross.watched();
|
||||
@@ -129,6 +158,47 @@ async function run() {
|
||||
session.setItem("aislo:draft:cross-display:p-1:12", "abc");
|
||||
await cross.refresh();
|
||||
out.badValue = { watched: cross.watched() };
|
||||
|
||||
// 382-2 발자국 기록 = 비탈 끝 좌 · 우 · 깎기 여부
|
||||
footprint = { records: [record("grid:5", 100, 7.5, -6, true, false)] };
|
||||
out.footprint = cross.watched();
|
||||
// 노선 고침 — 같은 열쇠 측점이 102 로 밀리고 발자국도 새로(열쇠로 다시 짚음)
|
||||
stations = [
|
||||
grid(2, 40),
|
||||
grid(3, 60),
|
||||
{ key: "pipe:a", kind: "pipe", chainageM: 80 },
|
||||
grid(5, 102),
|
||||
];
|
||||
footprint = { records: [record("grid:5", 102, 9, -4, false, true)] };
|
||||
route = { v: 2 };
|
||||
await cross.refresh();
|
||||
out.afterEdit = cross.watched();
|
||||
|
||||
// 평면 눈금 클릭 = 공용 고름에 적음 · 판이 접혀 있으면 폄
|
||||
const heard = [];
|
||||
pick.subscribeSelection("p-1", (selection, from) =>
|
||||
heard.push({ at: selection.chainageM, from }),
|
||||
);
|
||||
panels.now = null;
|
||||
await cross.open(41);
|
||||
out.planClick = {
|
||||
selection: pick.readSelection("p-1"),
|
||||
heard: heard.slice(),
|
||||
panel: panels.now,
|
||||
watched: cross.watched(),
|
||||
};
|
||||
|
||||
// 다른 보기 고름 — 일반측점 = 그 측점(판 폄) · 관 측점 · 구조물 id = 마름모 · 풂 = 없음
|
||||
panels.now = null;
|
||||
pick.selectDesign("p-1", { chainageM: 60, structureId: null }, { from: "b06-card" });
|
||||
out.otherGrid = { watched: cross.watched(), panel: panels.now };
|
||||
pick.selectDesign("p-1", { chainageM: 80, structureId: null }, { from: "profile" });
|
||||
out.otherPipe = cross.watched();
|
||||
pick.selectDesign("p-1", { chainageM: 60, structureId: "s-1" }, { from: "list" });
|
||||
out.otherStructure = cross.watched();
|
||||
const before = shownCount;
|
||||
pick.selectDesign("p-1", { chainageM: null, structureId: null }, { from: "escape" });
|
||||
out.cleared = { watched: cross.watched(), redrawn: shownCount > before };
|
||||
return out;
|
||||
}
|
||||
run().then((out) => console.log(JSON.stringify(out)));
|
||||
|
||||
@@ -48,7 +48,9 @@ def test_겉틀은_본문_칸():
|
||||
assert 'title="되돌리기 (Ctrl+Z)"' in chrome and 'title="다시하기 (Ctrl+Y)"' in chrome
|
||||
# 시험 · 배선이 집는 자리는 남김
|
||||
assert "b05-routeedit__measure-close" in chrome
|
||||
assert 'data-act="side-toggle"' in chrome and "is-collapsed" in chrome
|
||||
# 382-1 — 오른쪽 횡단 판 = 패널 양식(손잡이 · 머리 · 폭 끌기 · 열림 · 폭 기억) · 칸 높이를 채움
|
||||
assert "createSidePanels({" in chrome and 'prefKey: "b05-routeedit-cross"' in chrome
|
||||
assert "follow: false" in chrome and 'data-act="side-toggle"' not in chrome
|
||||
|
||||
|
||||
def test_CSS_덮개_걷음_대기막은_전체():
|
||||
@@ -60,9 +62,8 @@ def test_CSS_덮개_걷음_대기막은_전체():
|
||||
assert "position: fixed;" in busy and "z-index: var(--z-modal, 1000);" in busy
|
||||
label = _block(css, ".b05-routeedit__label")
|
||||
assert "position: absolute;" in label and "position: fixed" not in label
|
||||
assert "display: none;" in _block(
|
||||
css, ".b05-routeedit__side-wrap.is-collapsed > .b05-routeedit__side"
|
||||
)
|
||||
# 382-1 — 자체 [횡단] 단추 · 접기 걷음(접기는 양식 손잡이)
|
||||
assert "b05-routeedit__side-toggle" not in css and "is-side-open" not in css
|
||||
assert "@media (width < 1500px)" not in css # 접기는 단추로(처음만 폭으로)
|
||||
|
||||
|
||||
@@ -92,7 +93,7 @@ def test_352_1_머리줄_상자_없음_도구는_뜬_단추_횡단은_우측_덮
|
||||
assert "right: var(--spacing-12);" in _block(css, ".b05-routeedit__spin") # 도구줄 줄 오른쪽
|
||||
assert "34px" in side # 횡단 판은 도구줄 줄 아래에서 시작
|
||||
assert "var(--b05-side-open)" in _block(css, ".b05-routeedit__measure")
|
||||
assert "is-side-open" in chrome
|
||||
assert '"--b05-side-open"' in chrome # 판 폭(손잡이 포함)을 적음
|
||||
glass = _block(css, ".b05-routeedit__terrain")
|
||||
assert "backdrop-filter: blur(6px);" in glass and "background:" in glass
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""계획노선 편집 창 — **측점 반폭 선 길이**(계획서 22-1) 회귀검증.
|
||||
"""계획노선 편집 창 — **측점 법선 길이 · 고름 연동**(계획서 22-1 · 382-2) 회귀검증.
|
||||
|
||||
파란 선 = 우측 패널 「횡단 반폭(m)」 적용값 양쪽. 적용한 적이 없으면 원지반 선 범위
|
||||
법선 = 그 측점 발자국 비탈 끝 좌 · 우(382-2) · 기록이 없으면 「횡단 반폭(m)」 적용값 양쪽 · 그것도 없으면 원지반 선 범위
|
||||
(계산 묶음 그 측점 표본 · 365-3).
|
||||
Node 헬퍼(`helper_b05_routeedit_cross_width.cjs`)가 TS 를 트랜스파일해 돌린다.
|
||||
"""
|
||||
@@ -45,3 +45,50 @@ def test_노선이_여럿이면_최신_노선_값_다른_프로젝트는_무시(
|
||||
|
||||
def test_망가진_값은_기본값으로(results):
|
||||
assert results["badValue"]["watched"]["rightM"] == 20
|
||||
|
||||
|
||||
# ── 382-2 법선 = 발자국 비탈 끝 · 측점 열쇠 · 공용 고름 연동 ──────────────────────
|
||||
|
||||
|
||||
def test_발자국_기록이면_비탈_끝_좌우와_깎기(results):
|
||||
assert results["footprint"] == {
|
||||
"chainageM": 100,
|
||||
"leftM": 7.5,
|
||||
"rightM": 6,
|
||||
"leftCut": True,
|
||||
"rightCut": False,
|
||||
}
|
||||
|
||||
|
||||
def test_노선_고친_뒤_같은_열쇠_측점의_새_발자국(results):
|
||||
assert results["afterEdit"] == {
|
||||
"chainageM": 102,
|
||||
"leftM": 9,
|
||||
"rightM": 4,
|
||||
"leftCut": False,
|
||||
"rightCut": True,
|
||||
}
|
||||
|
||||
|
||||
def test_평면_눈금_클릭은_공용_고름에_적고_판을_편다(results):
|
||||
click = results["planClick"]
|
||||
assert click["selection"] == {"chainageM": 40, "structureId": None}
|
||||
assert click["heard"] == [{"at": 40, "from": "b05-plan"}]
|
||||
assert click["panel"] == "cross"
|
||||
assert click["watched"]["chainageM"] == 40
|
||||
|
||||
|
||||
def test_다른_보기_일반측점_고름은_평면_판(results):
|
||||
assert results["otherGrid"]["watched"]["chainageM"] == 60
|
||||
assert "structure" not in results["otherGrid"]["watched"]
|
||||
assert results["otherGrid"]["panel"] == "cross"
|
||||
|
||||
|
||||
def test_관_구조물_고름은_마름모만(results):
|
||||
assert results["otherPipe"] == {"chainageM": 80, "leftM": 0, "rightM": 0, "structure": True}
|
||||
assert results["otherStructure"]["structure"] is True
|
||||
assert results["otherStructure"]["chainageM"] == 60
|
||||
|
||||
|
||||
def test_고름을_풀면_법선도_없다(results):
|
||||
assert results["cleared"] == {"watched": None, "redrawn": True}
|
||||
|
||||
Reference in New Issue
Block a user