Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1

This commit is contained in:
2026-10-17 15:08:37 +09:00
8 changed files with 341 additions and 87 deletions
+29 -15
View File
@@ -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")!;
+132 -23
View File
@@ -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 };
},
};
}
+35 -9
View File
@@ -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();
}
+10 -24
View File
@@ -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}