diff --git a/A00_Common/design/design_pick_profile.ts b/A00_Common/design/design_pick_profile.ts index 48c2bb1bb..d8c3a4d07 100644 --- a/A00_Common/design/design_pick_profile.ts +++ b/A00_Common/design/design_pick_profile.ts @@ -1,6 +1,7 @@ /* ============================================================================= * A00_Common/design/design_pick_profile.ts - * 종단도 고름 강조(측점 세로선 · 알약 · 구간 띠) = 고름 한 칸에서 파생 (233-1 · 조사 AF (나) ②). + * 종단 보기 고름 강조 = 고름 한 칸에서 파생 — 종단도(측점 세로선 · 알약 · 구간 띠 · 233-1) · + * 3D(측점 띠 · 핀 · 부재 · 233-2) (조사 AF (나) ② ③). * * 고름 한 칸(`structure-pick`)이 유일 원천이다. 값이 바뀌면(구독) 그 값으로 종단도를 다시 세우고, * 관 · 구조물 목록이 바뀐 뒤에도 같은 값으로 다시 얹는다(`paint`) — 화면끼리 손으로 부르던 길이 @@ -70,18 +71,53 @@ export function paintProfilePick(target: ProfilePickTarget, pick: ProfilePick): else target.setSelectedStation(pick.stationId); } +export interface ViewerPickTarget { + markers: { selectStation: (stationId: string | null, notify?: boolean) => void }; + structurePick: { + select: (pick: { chainageM: number; kind: string; key?: string } | null) => void; + }; +} + +/** 고름 값 → 3D 가 켤 것 — 띠(+핀) = 종단 측점선과 같은 측점(구조물 고름은 그 자리 구조물 측점만) · + * 부재 = 고른 측점의 부재 전부(부재키가 있으면 그 부재 하나). */ +export function viewerPickOf( + selection: (DesignSelection & { partKey?: string }) | null, + source: ProfilePickSource, +): { stationId: string | null; member: { chainageM: number; kind: string; key?: string } | null } { + if (!selection || selection.chainageM === null) return { stationId: null, member: null }; + const at = selection.chainageM; + const pick = profilePickOf(selection, source); + const own = selection.structureId + ? nearestWithin(source.irregular(), at, (entry) => entry.chainage_m) + : undefined; + const stationId = "stationId" in pick ? pick.stationId : own ? irregularStationId(own.id) : null; + return { stationId, member: { chainageM: at, kind: "", key: selection.partKey } }; +} + +export function paintViewerPick( + target: ViewerPickTarget, + pick: ReturnType, +): void { + target.markers.selectStation(pick.stationId, false); // 칠하기만 — 알림이 사슬을 다시 돌리지 않게 + target.structurePick.select(pick.member); +} + /** - * 종단도를 고름 한 칸에 묶음 — 값이 바뀔 때마다 그림 · `paint()` = 목록이 바뀐 뒤 같은 값으로 다시 얹음. - * 보기가 숨어 있으면(다른 보기가 종단도를 쥠) 안 그림 — 그 보기가 제 규칙으로 그림 · 보기를 떠나면 구독 끊음. + * 종단 보기(종단도 · 3D)를 고름 한 칸에 묶음 — 값이 바뀔 때마다 그림 · `paint()` = 목록이 바뀐 뒤 · + * 폼이 닫힌 뒤 같은 값으로 다시 얹음. 보기가 숨어 있으면(횡단 보기가 종단도를 쥠) 안 그림 — 그 보기가 + * 제 규칙으로 그림 · 보기를 떠나면 구독 끊음. */ export function bindProfilePick( projectId: string | null, - target: ProfilePickTarget, + target: { profile: ProfilePickTarget; viewer?: ViewerPickTarget }, host: { visible: () => boolean; onDispose: (fn: () => void) => void }, source: ProfilePickSource, ): { paint: () => void } { const paint = (): void => { - if (host.visible()) paintProfilePick(target, profilePickOf(readSelection(projectId), source)); + if (!host.visible()) return; + const selection = readSelection(projectId); + paintProfilePick(target.profile, profilePickOf(selection, source)); + if (target.viewer) paintViewerPick(target.viewer, viewerPickOf(selection, source)); }; host.onDispose(subscribeSelection(projectId, paint)); return { paint }; diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 985c16f40..77aba5392 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -39,6 +39,7 @@ import { readStructurePick } from "../../B05_Profile/B05_Profile_UI_Structure_Pi import { NO_SELECTION, PIPE_MATCH_M, selectDesign } from "./design_selection"; import { readPendingPipes } from "../../B05_Profile/B05_Profile_Api_Pipes_Draft"; import { applyStructurePick } from "../../B06_Section/B06_Section_UI_Page_Structure_Pick"; +import { bindCrossPick } from "../../B06_Section/B06_Section_UI_Page_Pick_Sync"; import { createSectionView } from "../../B06_Section/B06_Section_UI_Section_View"; import { revetFormOf, revetWallSpec } from "../../B06_Section/B06_Section_UI_Cross_Culvert_Const"; import { @@ -387,7 +388,7 @@ export async function mountCrossView( : null; if (!target) { structuresPanel.showAtChainage(null); - selectDesign(projectId, NO_SELECTION, { from: "b06" }); // 카드 해제 — 선택도 비운다. + releaseCross(); // 카드 다시 누름 = 풀기 한 길(범위 테두리 · 형상까지 · 233-2) return; } const owner = isWallSelecting() ? stationControls.structureSpan.ownerOf(target) : null; @@ -494,23 +495,20 @@ export async function mountCrossView( if (applied) appliedPick = signature; } - /** 고름을 다 풂(54-38) — 카드 · 벽 · 세월교 · BOX 강조 · 넘김 되얹기 표시 · 선택 칸. */ + // 횡단 보기 강조 = 고름 한 칸 구독 · 다른 곳(Esc 등)이 바꾼 값으로 다시 세움(233-2) + const crossPick = bindCrossPick(projectId, host, { + sections: () => sectionDetail?.cross_sections ?? [], + controls: stationControls, + releasePipe: () => structuresPanel.releasePipe(), + view: sectionView, + reapply: () => { + appliedPick = null; + restoreStructurePick(); + }, + }); + /** 고름을 다 풂(54-38) — 강조 모두 끄고 넘김 되얹기 표시 · 선택 칸 비움(풀기 한 길 · 233-2). */ function releaseCross(): void { - const { revetOffset, ford, box } = stationControls; - const lit = (sectionDetail?.cross_sections ?? []).filter( - (s) => - revetOffset.highlightFor(s) || - ford.selectedFor(s.chainage_m) || - box.selectedFor(s.chainage_m), - ); - for (const s of lit) { - if (revetOffset.selectedFor(s)) revetOffset.select(s.chainage_m, null); - if (ford.selectedFor(s.chainage_m)) ford.select(s.chainage_m, null); - if (box.selectedFor(s.chainage_m)) box.select(s.chainage_m, null); - } - structuresPanel.releasePipe(); - sectionView.focusStation(null); - sectionView.refreshCards(lit.map((s) => s.chainage_m)); + crossPick.release(); appliedPick = null; selectDesign(projectId, NO_SELECTION, { from: "b06" }); } diff --git a/A00_Common/design/design_view_profile.ts b/A00_Common/design/design_view_profile.ts index 05821f65d..1adab405a 100644 --- a/A00_Common/design/design_view_profile.ts +++ b/A00_Common/design/design_view_profile.ts @@ -411,8 +411,8 @@ export async function mountProfileView( }, isRestoring: () => restoring, }); - // 종단도 강조 = 고름 한 칸에서 파생 · 이 보기가 보일 때만(횡단 보기는 제 규칙 · 233-1) - const profilePick = bindProfilePick(activeProjectId, profilePanel, host, { + // 종단도 · 3D 강조 = 고름 한 칸에서 파생 · 이 보기가 보일 때만(횡단 보기는 제 규칙 · 233-1 · 233-2) + const profilePick = bindProfilePick(activeProjectId, { profile: profilePanel, viewer }, host, { irregular: () => bridge.irregularStations(), pipes: () => lastPipes ?? [], sections: () => currentSectionDetail?.cross_sections ?? [], @@ -573,7 +573,6 @@ export async function mountProfileView( ); return; } - viewer.markers.selectStation(null); profilePick.paint(); }, // 보기를 떠나는 자리 = 코리도 영구저장 시점(2026-08-23 사용자 확정) — 이동은 막지 않는다. diff --git a/B05_Profile/B05_Profile_UI_Markers.ts b/B05_Profile/B05_Profile_UI_Markers.ts index e0709d413..a32733979 100644 --- a/B05_Profile/B05_Profile_UI_Markers.ts +++ b/B05_Profile/B05_Profile_UI_Markers.ts @@ -494,7 +494,8 @@ export function createRouteMarkers( if (center) selectionPin.position.copy(center); } - function selectStation(stationId: string | null): void { + /** notify 거짓 = 고름 한 칸에서 파생해 칠하기만(알림 없음 · 233-2). */ + function selectStation(stationId: string | null, notify = true): void { selectedStationId = stationId; stationGroup.children.forEach((object) => { // 측점 띠(stationId 보유 Mesh)만 색을 바꾼다. @@ -503,7 +504,7 @@ export function createRouteMarkers( (object.material as THREE.MeshBasicMaterial).color.set(selected ? 0xef4444 : 0xfacc15); }); syncSelectionPin(); - stationSelectionListener?.(selectedStationId); + if (notify) stationSelectionListener?.(selectedStationId); } return { diff --git a/B06_Section/B06_Section_UI_Cross_Detail_Select.ts b/B06_Section/B06_Section_UI_Cross_Detail_Select.ts index b3e8540ec..2a5aca824 100644 --- a/B06_Section/B06_Section_UI_Cross_Detail_Select.ts +++ b/B06_Section/B06_Section_UI_Cross_Detail_Select.ts @@ -143,7 +143,7 @@ export function selectionSource( /* ── 모달 배선 ─────────────────────────────────────────────────────────── */ export interface DetailSelect { - /** 그림을 새로 그린 뒤 — 조각 · 판정 선 · 값 칸 다시(고름 지움). */ + /** 그림을 새로 그린 뒤 — 조각 · 판정 선 · 값 칸 다시(같은 자리 조각 고름은 유지 · 233-2). */ redraw: () => void; /** 고르기 갈래(표 우클릭) — 있으면 [칸에 넣기] 단추 · 면적이면 누름 = 늘 더하기. */ setMode: (kind: "area" | "length" | null) => void; @@ -152,6 +152,10 @@ export interface DetailSelect { detach: () => void; } +/** 조각의 자리 이름 — 선 종류 + mm 로 맞춘 꼭짓점(px → m 되짚기의 부동소수 흔들림 거름). */ +const pieceKey = (piece: SplitPiece | undefined): string => + piece ? `${piece.line}|${piece.points.map((p) => p.map((v) => v.toFixed(3))).join(";")}` : ""; + export function attachDetailSelect(input: { cad: HTMLElement; design: () => CrossDesign | undefined; @@ -209,10 +213,12 @@ export function attachDetailSelect(input: { return { redraw() { - selected = []; + // 다시 그려도(창 크기 · 카드 다시 세움) 같은 자리 조각은 고름 유지 — 지우면 바로 꺼졌음(233-2) + const keep = new Set(selected.map((i) => pieceKey(pieces[i]))); const axes = input.axes(); const layer = cad.querySelector("g.b06-chart__plot-layer"); pieces = axes && layer ? collectPieces([...layer.querySelectorAll("*")], axes) : []; + selected = pieces.flatMap((piece, i) => (keep.has(pieceKey(piece)) ? [i] : [])); const toPx = axes ? mToPx(axes) : null; hits = toPx ? pieces.map((piece) => { diff --git a/B06_Section/B06_Section_UI_Page_Pick_Sync.ts b/B06_Section/B06_Section_UI_Page_Pick_Sync.ts new file mode 100644 index 000000000..48d3cad72 --- /dev/null +++ b/B06_Section/B06_Section_UI_Page_Pick_Sync.ts @@ -0,0 +1,80 @@ +/* ============================================================================= + * B06_Section_UI_Page_Pick_Sync.ts + * 횡단 보기 강조를 고름 한 칸에 묶음 (233-2 · 조사 AF (나) ④). + * + * 고름 한 칸(`structure-pick`)이 유일 원천이다. 횡단 보기가 보일 때 다른 곳(풀기 Esc 등)이 값을 + * 바꾸면 구독으로 받아 카드 · 범위 테두리(`--span`) · 포장층 · 벽 · 세월교 · BOX 형상 강조를 그 값으로 + * 다시 세운다 — 값이 비면 모두 풂, 값이 있으면 넘김 적용(`applyStructurePick`)과 같은 길로 다시 얹음. + * 횡단 보기 안 누름(카드 · 목록 · 알약 · 형상)은 그 자리에서 그리고 값을 적으므로(from "b06") 되받지 않음. + * 풀기(카드 다시 누름 · 목록 다시 누름 · 보기 오감)는 `release` 한 길 — 카드 · 폼 관 · 범위 테두리 · + * 형상까지 모두 끔(카드만 풀고 `--span` 이 남던 구멍 · a4). + * ========================================================================== */ + +import { subscribeSelection } from "../A00_Common/design/design_selection"; +import type { CrossSection } from "./B06_Section_Api_Fetch"; +import type { StationControls } from "./B06_Section_UI_Page_Station_Controls"; + +export interface CrossPickDeps { + sections: () => ReadonlyArray; + controls: Pick; + releasePipe: () => void; + view: { + focusStation: (stationId: string | null) => void; + refreshCards: (chainages: ReadonlyArray) => void; + selectMark: (structureId: string | null, span?: boolean) => void; + }; + /** 지금 값으로 카드 · 형상 · 폼을 다시 세움(넘김 적용 길). */ + reapply: () => void; +} + +let releasing = false; + +/** 횡단 보기 강조를 모두 끔 — 카드 · 벽 · 세월교 · BOX · 폼 관 · 범위 테두리 · 포장층(값은 안 건드림). + * 카드를 풀면 카드 리스너가 다시 풀기를 부름 — 되돌아온 부름은 건너뜀. */ +export function releaseCrossHighlights(deps: CrossPickDeps): void { + if (releasing) return; + releasing = true; + try { + releaseAll(deps); + } finally { + releasing = false; + } +} + +function releaseAll(deps: CrossPickDeps): void { + const { revetOffset, ford, box } = deps.controls; + const lit = deps + .sections() + .filter( + (s) => + revetOffset.highlightFor(s) || + ford.selectedFor(s.chainage_m) || + box.selectedFor(s.chainage_m), + ); + for (const s of lit) { + if (revetOffset.selectedFor(s)) revetOffset.select(s.chainage_m, null); + if (ford.selectedFor(s.chainage_m)) ford.select(s.chainage_m, null); + if (box.selectedFor(s.chainage_m)) box.select(s.chainage_m, null); + } + deps.releasePipe(); + deps.view.focusStation(null); + deps.view.selectMark(null); + deps.view.refreshCards(lit.map((s) => s.chainage_m)); +} + +/** 횡단 보기를 고름 한 칸에 묶음 — 보일 때 · 다른 곳이 바꾼 값만 받음 · 보기를 떠나면 끊음. + * `release` = 강조 모두 끔(값은 부르는 쪽이 비움). */ +export function bindCrossPick( + projectId: string | null, + host: { visible: () => boolean; onDispose: (fn: () => void) => void }, + deps: CrossPickDeps, +): { release: () => void } { + host.onDispose( + subscribeSelection(projectId, (selection, from) => { + if (!host.visible() || from === "b06") return; + if (selection.chainageM === null) releaseCrossHighlights(deps); + else deps.reapply(); + }), + ); + return { release: () => releaseCrossHighlights(deps) }; +} diff --git a/resources/tester/fixtures/b05_213_1_hidden_station_longitudinal.json b/resources/tester/fixtures/b05_213_1_hidden_station_longitudinal.json new file mode 100644 index 000000000..38caa4025 --- /dev/null +++ b/resources/tester/fixtures/b05_213_1_hidden_station_longitudinal.json @@ -0,0 +1,2261 @@ +{ + "_note": "213-1 시험 고정 자료 — 엄상준_검증 d2245e3b initial_snapshot 종단 160~360m 사본, 관 측점 263.206 → 263.21(화면 0.01m 반올림) · 꼭짓점은 옛 글자 263.206 · 편집 키 263.206 · 263.210 둘(213-1 이전 재현 상태)", + "length_m": 1196.88459, + "samples": [ + { + "chainage_m": 160.0, + "x": 208393.562479, + "y": 367768.040752, + "z": 880.274875, + "elevation_m": 880.274875, + "valid": true + }, + { + "chainage_m": 161.0, + "x": 208394.009662, + "y": 367767.14631, + "z": 880.066701, + "elevation_m": 880.066701, + "valid": true + }, + { + "chainage_m": 162.0, + "x": 208394.456845, + "y": 367766.251867, + "z": 879.974874, + "elevation_m": 879.974874, + "valid": true + }, + { + "chainage_m": 163.0, + "x": 208394.904028, + "y": 367765.357425, + "z": 879.939525, + "elevation_m": 879.939525, + "valid": true + }, + { + "chainage_m": 164.0, + "x": 208395.351211, + "y": 367764.462982, + "z": 879.783039, + "elevation_m": 879.783039, + "valid": true + }, + { + "chainage_m": 165.0, + "x": 208395.798394, + "y": 367763.56854, + "z": 879.522523, + "elevation_m": 879.522523, + "valid": true + }, + { + "chainage_m": 166.0, + "x": 208396.245578, + "y": 367762.674097, + "z": 879.721375, + "elevation_m": 879.721375, + "valid": true + }, + { + "chainage_m": 167.0, + "x": 208396.692761, + "y": 367761.779655, + "z": 879.909006, + "elevation_m": 879.909006, + "valid": true + }, + { + "chainage_m": 168.0, + "x": 208397.139944, + "y": 367760.885212, + "z": 880.000277, + "elevation_m": 880.000277, + "valid": true + }, + { + "chainage_m": 169.0, + "x": 208397.587127, + "y": 367759.99077, + "z": 880.150005, + "elevation_m": 880.150005, + "valid": true + }, + { + "chainage_m": 170.0, + "x": 208398.067013, + "y": 367759.113789, + "z": 880.207492, + "elevation_m": 880.207492, + "valid": true + }, + { + "chainage_m": 171.0, + "x": 208398.598118, + "y": 367758.268125, + "z": 879.901536, + "elevation_m": 879.901536, + "valid": true + }, + { + "chainage_m": 172.0, + "x": 208399.192951, + "y": 367757.465031, + "z": 879.853283, + "elevation_m": 879.853283, + "valid": true + }, + { + "chainage_m": 173.0, + "x": 208399.861358, + "y": 367756.721235, + "z": 879.748083, + "elevation_m": 879.748083, + "valid": true + }, + { + "chainage_m": 174.0, + "x": 208400.562, + "y": 367756.008, + "z": 879.519311, + "elevation_m": 879.519311, + "valid": true + }, + { + "chainage_m": 175.0, + "x": 208401.269107, + "y": 367755.300893, + "z": 879.336245, + "elevation_m": 879.336245, + "valid": true + }, + { + "chainage_m": 176.0, + "x": 208401.976214, + "y": 367754.593786, + "z": 879.025666, + "elevation_m": 879.025666, + "valid": true + }, + { + "chainage_m": 177.0, + "x": 208402.683321, + "y": 367753.886679, + "z": 878.89015, + "elevation_m": 878.89015, + "valid": true + }, + { + "chainage_m": 178.0, + "x": 208403.390427, + "y": 367753.179573, + "z": 878.965976, + "elevation_m": 878.965976, + "valid": true + }, + { + "chainage_m": 179.0, + "x": 208404.097534, + "y": 367752.472466, + "z": 879.083304, + "elevation_m": 879.083304, + "valid": true + }, + { + "chainage_m": 180.0, + "x": 208404.804641, + "y": 367751.765359, + "z": 878.960423, + "elevation_m": 878.960423, + "valid": true + }, + { + "chainage_m": 181.0, + "x": 208405.526937, + "y": 367751.074275, + "z": 879.03746, + "elevation_m": 879.03746, + "valid": true + }, + { + "chainage_m": 182.0, + "x": 208406.279346, + "y": 367750.416559, + "z": 878.932674, + "elevation_m": 878.932674, + "valid": true + }, + { + "chainage_m": 183.0, + "x": 208407.090763, + "y": 367749.832091, + "z": 879.027649, + "elevation_m": 879.027649, + "valid": true + }, + { + "chainage_m": 184.0, + "x": 208407.950598, + "y": 367749.323011, + "z": 879.307757, + "elevation_m": 879.307757, + "valid": true + }, + { + "chainage_m": 185.0, + "x": 208408.833749, + "y": 367748.854692, + "z": 879.54669, + "elevation_m": 879.54669, + "valid": true + }, + { + "chainage_m": 186.0, + "x": 208409.728224, + "y": 367748.407575, + "z": 879.538158, + "elevation_m": 879.538158, + "valid": true + }, + { + "chainage_m": 187.0, + "x": 208410.6227, + "y": 367747.960457, + "z": 879.281805, + "elevation_m": 879.281805, + "valid": true + }, + { + "chainage_m": 188.0, + "x": 208411.517175, + "y": 367747.513339, + "z": 878.973901, + "elevation_m": 878.973901, + "valid": true + }, + { + "chainage_m": 189.0, + "x": 208412.419339, + "y": 367747.082534, + "z": 878.77534, + "elevation_m": 878.77534, + "valid": true + }, + { + "chainage_m": 190.0, + "x": 208413.342822, + "y": 367746.701142, + "z": 878.869338, + "elevation_m": 878.869338, + "valid": true + }, + { + "chainage_m": 191.0, + "x": 208414.294782, + "y": 367746.396907, + "z": 879.691937, + "elevation_m": 879.691937, + "valid": true + }, + { + "chainage_m": 192.0, + "x": 208415.269068, + "y": 367746.172536, + "z": 880.470388, + "elevation_m": 880.470388, + "valid": true + }, + { + "chainage_m": 193.0, + "x": 208416.259321, + "y": 367746.033253, + "z": 880.612699, + "elevation_m": 880.612699, + "valid": true + }, + { + "chainage_m": 194.0, + "x": 208417.257753, + "y": 367745.981861, + "z": 881.503248, + "elevation_m": 881.503248, + "valid": true + }, + { + "chainage_m": 195.0, + "x": 208418.257573, + "y": 367745.974, + "z": 882.19521, + "elevation_m": 882.19521, + "valid": true + }, + { + "chainage_m": 196.0, + "x": 208419.257573, + "y": 367745.974, + "z": 882.327939, + "elevation_m": 882.327939, + "valid": true + }, + { + "chainage_m": 197.0, + "x": 208420.257573, + "y": 367745.974, + "z": 882.119903, + "elevation_m": 882.119903, + "valid": true + }, + { + "chainage_m": 198.0, + "x": 208421.257573, + "y": 367745.974, + "z": 880.314188, + "elevation_m": 880.314188, + "valid": true + }, + { + "chainage_m": 199.0, + "x": 208422.257573, + "y": 367745.974, + "z": 878.282163, + "elevation_m": 878.282163, + "valid": true + }, + { + "chainage_m": 200.0, + "x": 208423.257573, + "y": 367745.974, + "z": 877.309908, + "elevation_m": 877.309908, + "valid": true + }, + { + "chainage_m": 201.0, + "x": 208424.257573, + "y": 367745.974, + "z": 877.12456, + "elevation_m": 877.12456, + "valid": true + }, + { + "chainage_m": 202.0, + "x": 208425.257573, + "y": 367745.974, + "z": 877.052589, + "elevation_m": 877.052589, + "valid": true + }, + { + "chainage_m": 203.0, + "x": 208426.257573, + "y": 367745.974, + "z": 876.634767, + "elevation_m": 876.634767, + "valid": true + }, + { + "chainage_m": 204.0, + "x": 208427.257573, + "y": 367745.974, + "z": 876.139693, + "elevation_m": 876.139693, + "valid": true + }, + { + "chainage_m": 205.0, + "x": 208428.257573, + "y": 367745.974, + "z": 876.149218, + "elevation_m": 876.149218, + "valid": true + }, + { + "chainage_m": 206.0, + "x": 208429.257573, + "y": 367745.974, + "z": 876.096325, + "elevation_m": 876.096325, + "valid": true + }, + { + "chainage_m": 207.0, + "x": 208430.257573, + "y": 367745.974, + "z": 875.870445, + "elevation_m": 875.870445, + "valid": true + }, + { + "chainage_m": 208.0, + "x": 208431.257573, + "y": 367745.974, + "z": 875.785863, + "elevation_m": 875.785863, + "valid": true + }, + { + "chainage_m": 209.0, + "x": 208432.257573, + "y": 367745.974, + "z": 875.788061, + "elevation_m": 875.788061, + "valid": true + }, + { + "chainage_m": 210.0, + "x": 208433.257573, + "y": 367745.974, + "z": 875.853658, + "elevation_m": 875.853658, + "valid": true + }, + { + "chainage_m": 211.0, + "x": 208434.257573, + "y": 367745.974, + "z": 875.661856, + "elevation_m": 875.661856, + "valid": true + }, + { + "chainage_m": 212.0, + "x": 208435.257573, + "y": 367745.974, + "z": 875.290313, + "elevation_m": 875.290313, + "valid": true + }, + { + "chainage_m": 213.0, + "x": 208436.257573, + "y": 367745.974, + "z": 875.127237, + "elevation_m": 875.127237, + "valid": true + }, + { + "chainage_m": 214.0, + "x": 208437.257573, + "y": 367745.974, + "z": 874.938191, + "elevation_m": 874.938191, + "valid": true + }, + { + "chainage_m": 215.0, + "x": 208438.257573, + "y": 367745.974, + "z": 874.889529, + "elevation_m": 874.889529, + "valid": true + }, + { + "chainage_m": 216.0, + "x": 208439.257573, + "y": 367745.974, + "z": 875.186213, + "elevation_m": 875.186213, + "valid": true + }, + { + "chainage_m": 217.0, + "x": 208440.257573, + "y": 367745.974, + "z": 875.477006, + "elevation_m": 875.477006, + "valid": true + }, + { + "chainage_m": 218.0, + "x": 208441.257573, + "y": 367745.974, + "z": 875.443878, + "elevation_m": 875.443878, + "valid": true + }, + { + "chainage_m": 219.0, + "x": 208442.257573, + "y": 367745.974, + "z": 875.186807, + "elevation_m": 875.186807, + "valid": true + }, + { + "chainage_m": 220.0, + "x": 208443.257573, + "y": 367745.974, + "z": 875.077858, + "elevation_m": 875.077858, + "valid": true + }, + { + "chainage_m": 221.0, + "x": 208444.257573, + "y": 367745.974, + "z": 875.625482, + "elevation_m": 875.625482, + "valid": true + }, + { + "chainage_m": 222.0, + "x": 208445.257573, + "y": 367745.974, + "z": 875.758555, + "elevation_m": 875.758555, + "valid": true + }, + { + "chainage_m": 223.0, + "x": 208446.257573, + "y": 367745.974, + "z": 875.482925, + "elevation_m": 875.482925, + "valid": true + }, + { + "chainage_m": 224.0, + "x": 208447.257573, + "y": 367745.974, + "z": 875.712811, + "elevation_m": 875.712811, + "valid": true + }, + { + "chainage_m": 225.0, + "x": 208448.257053, + "y": 367745.996679, + "z": 875.756986, + "elevation_m": 875.756986, + "valid": true + }, + { + "chainage_m": 226.0, + "x": 208449.252676, + "y": 367746.078262, + "z": 875.40165, + "elevation_m": 875.40165, + "valid": true + }, + { + "chainage_m": 227.0, + "x": 208450.238368, + "y": 367746.241833, + "z": 875.503535, + "elevation_m": 875.503535, + "valid": true + }, + { + "chainage_m": 228.0, + "x": 208451.207667, + "y": 367746.485362, + "z": 875.628622, + "elevation_m": 875.628622, + "valid": true + }, + { + "chainage_m": 229.0, + "x": 208452.153942, + "y": 367746.808156, + "z": 875.624947, + "elevation_m": 875.624947, + "valid": true + }, + { + "chainage_m": 230.0, + "x": 208453.068087, + "y": 367747.213544, + "z": 875.447687, + "elevation_m": 875.447687, + "valid": true + }, + { + "chainage_m": 231.0, + "x": 208453.963849, + "y": 367747.657925, + "z": 874.69746, + "elevation_m": 874.69746, + "valid": true + }, + { + "chainage_m": 232.0, + "x": 208454.858277, + "y": 367748.105138, + "z": 873.796567, + "elevation_m": 873.796567, + "valid": true + }, + { + "chainage_m": 233.0, + "x": 208455.752704, + "y": 367748.552352, + "z": 873.630186, + "elevation_m": 873.630186, + "valid": true + }, + { + "chainage_m": 234.0, + "x": 208456.647131, + "y": 367748.999565, + "z": 873.355806, + "elevation_m": 873.355806, + "valid": true + }, + { + "chainage_m": 235.0, + "x": 208457.541558, + "y": 367749.446779, + "z": 872.944746, + "elevation_m": 872.944746, + "valid": true + }, + { + "chainage_m": 236.0, + "x": 208458.435985, + "y": 367749.893993, + "z": 872.594761, + "elevation_m": 872.594761, + "valid": true + }, + { + "chainage_m": 237.0, + "x": 208459.330413, + "y": 367750.341206, + "z": 872.323602, + "elevation_m": 872.323602, + "valid": true + }, + { + "chainage_m": 238.0, + "x": 208460.22484, + "y": 367750.78842, + "z": 871.908658, + "elevation_m": 871.908658, + "valid": true + }, + { + "chainage_m": 239.0, + "x": 208461.119267, + "y": 367751.235633, + "z": 871.521665, + "elevation_m": 871.521665, + "valid": true + }, + { + "chainage_m": 240.0, + "x": 208462.013694, + "y": 367751.682847, + "z": 871.348918, + "elevation_m": 871.348918, + "valid": true + }, + { + "chainage_m": 241.0, + "x": 208462.908121, + "y": 367752.130061, + "z": 871.178815, + "elevation_m": 871.178815, + "valid": true + }, + { + "chainage_m": 242.0, + "x": 208463.802548, + "y": 367752.577274, + "z": 870.5225, + "elevation_m": 870.5225, + "valid": true + }, + { + "chainage_m": 243.0, + "x": 208464.696976, + "y": 367753.024488, + "z": 869.978886, + "elevation_m": 869.978886, + "valid": true + }, + { + "chainage_m": 244.0, + "x": 208465.591403, + "y": 367753.471701, + "z": 869.385986, + "elevation_m": 869.385986, + "valid": true + }, + { + "chainage_m": 245.0, + "x": 208466.48583, + "y": 367753.918915, + "z": 868.741635, + "elevation_m": 868.741635, + "valid": true + }, + { + "chainage_m": 246.0, + "x": 208467.380257, + "y": 367754.366129, + "z": 868.389385, + "elevation_m": 868.389385, + "valid": true + }, + { + "chainage_m": 247.0, + "x": 208468.274684, + "y": 367754.813342, + "z": 867.839514, + "elevation_m": 867.839514, + "valid": true + }, + { + "chainage_m": 248.0, + "x": 208469.169112, + "y": 367755.260556, + "z": 867.749827, + "elevation_m": 867.749827, + "valid": true + }, + { + "chainage_m": 249.0, + "x": 208470.063539, + "y": 367755.707769, + "z": 867.725932, + "elevation_m": 867.725932, + "valid": true + }, + { + "chainage_m": 250.0, + "x": 208470.957966, + "y": 367756.154983, + "z": 867.237421, + "elevation_m": 867.237421, + "valid": true + }, + { + "chainage_m": 251.0, + "x": 208471.852393, + "y": 367756.602197, + "z": 867.328371, + "elevation_m": 867.328371, + "valid": true + }, + { + "chainage_m": 252.0, + "x": 208472.74682, + "y": 367757.04941, + "z": 866.683088, + "elevation_m": 866.683088, + "valid": true + }, + { + "chainage_m": 253.0, + "x": 208473.641248, + "y": 367757.496624, + "z": 865.303387, + "elevation_m": 865.303387, + "valid": true + }, + { + "chainage_m": 254.0, + "x": 208474.535675, + "y": 367757.943837, + "z": 864.735997, + "elevation_m": 864.735997, + "valid": true + }, + { + "chainage_m": 255.0, + "x": 208475.430102, + "y": 367758.391051, + "z": 864.7865, + "elevation_m": 864.7865, + "valid": true + }, + { + "chainage_m": 256.0, + "x": 208476.324529, + "y": 367758.838265, + "z": 864.81515, + "elevation_m": 864.81515, + "valid": true + }, + { + "chainage_m": 257.0, + "x": 208477.218956, + "y": 367759.285478, + "z": 864.240971, + "elevation_m": 864.240971, + "valid": true + }, + { + "chainage_m": 258.0, + "x": 208478.113384, + "y": 367759.732692, + "z": 864.200528, + "elevation_m": 864.200528, + "valid": true + }, + { + "chainage_m": 259.0, + "x": 208479.007811, + "y": 367760.179905, + "z": 864.034127, + "elevation_m": 864.034127, + "valid": true + }, + { + "chainage_m": 260.0, + "x": 208479.902238, + "y": 367760.627119, + "z": 863.832762, + "elevation_m": 863.832762, + "valid": true + }, + { + "chainage_m": 261.0, + "x": 208480.796665, + "y": 367761.074333, + "z": 863.496394, + "elevation_m": 863.496394, + "valid": true + }, + { + "chainage_m": 262.0, + "x": 208481.691092, + "y": 367761.521546, + "z": 862.687865, + "elevation_m": 862.687865, + "valid": true + }, + { + "chainage_m": 263.0, + "x": 208482.585519, + "y": 367761.96876, + "z": 862.708177, + "elevation_m": 862.708177, + "valid": true + }, + { + "chainage_m": 264.0, + "x": 208483.479947, + "y": 367762.415973, + "z": 862.616929, + "elevation_m": 862.616929, + "valid": true + }, + { + "chainage_m": 265.0, + "x": 208484.374374, + "y": 367762.863187, + "z": 862.370077, + "elevation_m": 862.370077, + "valid": true + }, + { + "chainage_m": 266.0, + "x": 208485.268801, + "y": 367763.310401, + "z": 862.183629, + "elevation_m": 862.183629, + "valid": true + }, + { + "chainage_m": 267.0, + "x": 208486.163228, + "y": 367763.757614, + "z": 862.068982, + "elevation_m": 862.068982, + "valid": true + }, + { + "chainage_m": 268.0, + "x": 208487.057655, + "y": 367764.204828, + "z": 862.077233, + "elevation_m": 862.077233, + "valid": true + }, + { + "chainage_m": 269.0, + "x": 208487.952083, + "y": 367764.652041, + "z": 861.94402, + "elevation_m": 861.94402, + "valid": true + }, + { + "chainage_m": 270.0, + "x": 208488.863804, + "y": 367765.062569, + "z": 861.552265, + "elevation_m": 861.552265, + "valid": true + }, + { + "chainage_m": 271.0, + "x": 208489.803805, + "y": 367765.401435, + "z": 861.348876, + "elevation_m": 861.348876, + "valid": true + }, + { + "chainage_m": 272.0, + "x": 208490.768228, + "y": 367765.662155, + "z": 861.811783, + "elevation_m": 861.811783, + "valid": true + }, + { + "chainage_m": 273.0, + "x": 208491.750693, + "y": 367765.842721, + "z": 862.30889, + "elevation_m": 862.30889, + "valid": true + }, + { + "chainage_m": 274.0, + "x": 208492.744713, + "y": 367765.941972, + "z": 862.347235, + "elevation_m": 862.347235, + "valid": true + }, + { + "chainage_m": 275.0, + "x": 208493.743964, + "y": 367765.974, + "z": 862.18813, + "elevation_m": 862.18813, + "valid": true + }, + { + "chainage_m": 276.0, + "x": 208494.743964, + "y": 367765.974, + "z": 862.329525, + "elevation_m": 862.329525, + "valid": true + }, + { + "chainage_m": 277.0, + "x": 208495.743964, + "y": 367765.974, + "z": 862.652236, + "elevation_m": 862.652236, + "valid": true + }, + { + "chainage_m": 278.0, + "x": 208496.743964, + "y": 367765.974, + "z": 862.779696, + "elevation_m": 862.779696, + "valid": true + }, + { + "chainage_m": 279.0, + "x": 208497.743964, + "y": 367765.974, + "z": 862.697969, + "elevation_m": 862.697969, + "valid": true + }, + { + "chainage_m": 280.0, + "x": 208498.743964, + "y": 367765.974, + "z": 862.784431, + "elevation_m": 862.784431, + "valid": true + }, + { + "chainage_m": 281.0, + "x": 208499.743964, + "y": 367765.974, + "z": 863.016989, + "elevation_m": 863.016989, + "valid": true + }, + { + "chainage_m": 282.0, + "x": 208500.743964, + "y": 367765.974, + "z": 863.06037, + "elevation_m": 863.06037, + "valid": true + }, + { + "chainage_m": 283.0, + "x": 208501.743964, + "y": 367765.974, + "z": 863.169186, + "elevation_m": 863.169186, + "valid": true + }, + { + "chainage_m": 284.0, + "x": 208502.743964, + "y": 367765.974, + "z": 863.309619, + "elevation_m": 863.309619, + "valid": true + }, + { + "chainage_m": 285.0, + "x": 208503.743964, + "y": 367765.974, + "z": 863.551038, + "elevation_m": 863.551038, + "valid": true + }, + { + "chainage_m": 286.0, + "x": 208504.743964, + "y": 367765.974, + "z": 863.764148, + "elevation_m": 863.764148, + "valid": true + }, + { + "chainage_m": 287.0, + "x": 208505.743964, + "y": 367765.974, + "z": 863.89876, + "elevation_m": 863.89876, + "valid": true + }, + { + "chainage_m": 288.0, + "x": 208506.743964, + "y": 367765.974, + "z": 864.164324, + "elevation_m": 864.164324, + "valid": true + }, + { + "chainage_m": 289.0, + "x": 208507.743964, + "y": 367765.974, + "z": 864.336218, + "elevation_m": 864.336218, + "valid": true + }, + { + "chainage_m": 290.0, + "x": 208508.742891, + "y": 367765.928137, + "z": 864.070881, + "elevation_m": 864.070881, + "valid": true + }, + { + "chainage_m": 291.0, + "x": 208509.734387, + "y": 367765.801764, + "z": 864.008954, + "elevation_m": 864.008954, + "valid": true + }, + { + "chainage_m": 292.0, + "x": 208510.711727, + "y": 367765.593928, + "z": 864.503795, + "elevation_m": 864.503795, + "valid": true + }, + { + "chainage_m": 293.0, + "x": 208511.668704, + "y": 367765.307081, + "z": 864.617185, + "elevation_m": 864.617185, + "valid": true + }, + { + "chainage_m": 294.0, + "x": 208512.598739, + "y": 367764.942594, + "z": 864.949289, + "elevation_m": 864.949289, + "valid": true + }, + { + "chainage_m": 295.0, + "x": 208513.504625, + "y": 367764.519687, + "z": 865.163788, + "elevation_m": 865.163788, + "valid": true + }, + { + "chainage_m": 296.0, + "x": 208514.399052, + "y": 367764.072474, + "z": 865.441189, + "elevation_m": 865.441189, + "valid": true + }, + { + "chainage_m": 297.0, + "x": 208515.29348, + "y": 367763.62526, + "z": 865.797692, + "elevation_m": 865.797692, + "valid": true + }, + { + "chainage_m": 298.0, + "x": 208516.187907, + "y": 367763.178047, + "z": 866.464496, + "elevation_m": 866.464496, + "valid": true + }, + { + "chainage_m": 299.0, + "x": 208517.075808, + "y": 367762.718621, + "z": 867.214534, + "elevation_m": 867.214534, + "valid": true + }, + { + "chainage_m": 300.0, + "x": 208517.944672, + "y": 367762.22357, + "z": 867.864402, + "elevation_m": 867.864402, + "valid": true + }, + { + "chainage_m": 301.0, + "x": 208518.757896, + "y": 367761.641914, + "z": 868.228745, + "elevation_m": 868.228745, + "valid": true + }, + { + "chainage_m": 302.0, + "x": 208519.523465, + "y": 367761.000501, + "z": 868.587501, + "elevation_m": 868.587501, + "valid": true + }, + { + "chainage_m": 303.0, + "x": 208520.252968, + "y": 367760.317032, + "z": 868.710721, + "elevation_m": 868.710721, + "valid": true + }, + { + "chainage_m": 304.0, + "x": 208520.960075, + "y": 367759.609925, + "z": 870.026336, + "elevation_m": 870.026336, + "valid": true + }, + { + "chainage_m": 305.0, + "x": 208521.667182, + "y": 367758.902818, + "z": 870.609514, + "elevation_m": 870.609514, + "valid": true + }, + { + "chainage_m": 306.0, + "x": 208522.374288, + "y": 367758.195712, + "z": 871.522133, + "elevation_m": 871.522133, + "valid": true + }, + { + "chainage_m": 307.0, + "x": 208523.081395, + "y": 367757.488605, + "z": 871.571255, + "elevation_m": 871.571255, + "valid": true + }, + { + "chainage_m": 308.0, + "x": 208523.788502, + "y": 367756.781498, + "z": 871.704316, + "elevation_m": 871.704316, + "valid": true + }, + { + "chainage_m": 309.0, + "x": 208524.495609, + "y": 367756.074391, + "z": 872.173686, + "elevation_m": 872.173686, + "valid": true + }, + { + "chainage_m": 310.0, + "x": 208525.202715, + "y": 367755.367285, + "z": 872.223643, + "elevation_m": 872.223643, + "valid": true + }, + { + "chainage_m": 311.0, + "x": 208525.871593, + "y": 367754.623956, + "z": 872.384133, + "elevation_m": 872.384133, + "valid": true + }, + { + "chainage_m": 312.0, + "x": 208526.482219, + "y": 367753.833569, + "z": 872.308009, + "elevation_m": 872.308009, + "valid": true + }, + { + "chainage_m": 313.0, + "x": 208527.030651, + "y": 367752.999042, + "z": 871.762187, + "elevation_m": 871.762187, + "valid": true + }, + { + "chainage_m": 314.0, + "x": 208527.51956, + "y": 367752.126878, + "z": 871.648424, + "elevation_m": 871.648424, + "valid": true + }, + { + "chainage_m": 315.0, + "x": 208527.966769, + "y": 367751.232448, + "z": 871.724875, + "elevation_m": 871.724875, + "valid": true + }, + { + "chainage_m": 316.0, + "x": 208528.413977, + "y": 367750.338019, + "z": 871.83015, + "elevation_m": 871.83015, + "valid": true + }, + { + "chainage_m": 317.0, + "x": 208528.861185, + "y": 367749.443589, + "z": 871.535179, + "elevation_m": 871.535179, + "valid": true + }, + { + "chainage_m": 318.0, + "x": 208529.308394, + "y": 367748.549159, + "z": 871.494106, + "elevation_m": 871.494106, + "valid": true + }, + { + "chainage_m": 319.0, + "x": 208529.755602, + "y": 367747.654729, + "z": 871.29612, + "elevation_m": 871.29612, + "valid": true + }, + { + "chainage_m": 320.0, + "x": 208530.20281, + "y": 367746.760299, + "z": 871.872531, + "elevation_m": 871.872531, + "valid": true + }, + { + "chainage_m": 321.0, + "x": 208530.650019, + "y": 367745.86587, + "z": 872.09275, + "elevation_m": 872.09275, + "valid": true + }, + { + "chainage_m": 322.0, + "x": 208531.097227, + "y": 367744.97144, + "z": 872.006481, + "elevation_m": 872.006481, + "valid": true + }, + { + "chainage_m": 323.0, + "x": 208531.544436, + "y": 367744.07701, + "z": 871.972749, + "elevation_m": 871.972749, + "valid": true + }, + { + "chainage_m": 324.0, + "x": 208531.991644, + "y": 367743.18258, + "z": 871.819964, + "elevation_m": 871.819964, + "valid": true + }, + { + "chainage_m": 325.0, + "x": 208532.438852, + "y": 367742.28815, + "z": 871.483305, + "elevation_m": 871.483305, + "valid": true + }, + { + "chainage_m": 326.0, + "x": 208532.886061, + "y": 367741.393721, + "z": 870.837869, + "elevation_m": 870.837869, + "valid": true + }, + { + "chainage_m": 327.0, + "x": 208533.333269, + "y": 367740.499291, + "z": 870.770714, + "elevation_m": 870.770714, + "valid": true + }, + { + "chainage_m": 328.0, + "x": 208533.780477, + "y": 367739.604861, + "z": 870.903201, + "elevation_m": 870.903201, + "valid": true + }, + { + "chainage_m": 329.0, + "x": 208534.227686, + "y": 367738.710431, + "z": 870.158915, + "elevation_m": 870.158915, + "valid": true + }, + { + "chainage_m": 330.0, + "x": 208534.674894, + "y": 367737.816001, + "z": 870.2466, + "elevation_m": 870.2466, + "valid": true + }, + { + "chainage_m": 331.0, + "x": 208535.122103, + "y": 367736.921571, + "z": 870.481656, + "elevation_m": 870.481656, + "valid": true + }, + { + "chainage_m": 332.0, + "x": 208535.569311, + "y": 367736.027142, + "z": 870.637386, + "elevation_m": 870.637386, + "valid": true + }, + { + "chainage_m": 333.0, + "x": 208536.016519, + "y": 367735.132712, + "z": 870.740964, + "elevation_m": 870.740964, + "valid": true + }, + { + "chainage_m": 334.0, + "x": 208536.463728, + "y": 367734.238282, + "z": 870.315482, + "elevation_m": 870.315482, + "valid": true + }, + { + "chainage_m": 335.0, + "x": 208536.910936, + "y": 367733.343852, + "z": 870.455116, + "elevation_m": 870.455116, + "valid": true + }, + { + "chainage_m": 336.0, + "x": 208537.358144, + "y": 367732.449422, + "z": 870.773586, + "elevation_m": 870.773586, + "valid": true + }, + { + "chainage_m": 337.0, + "x": 208537.805353, + "y": 367731.554993, + "z": 871.170461, + "elevation_m": 871.170461, + "valid": true + }, + { + "chainage_m": 338.0, + "x": 208538.252561, + "y": 367730.660563, + "z": 870.856249, + "elevation_m": 870.856249, + "valid": true + }, + { + "chainage_m": 339.0, + "x": 208538.69977, + "y": 367729.766133, + "z": 870.772348, + "elevation_m": 870.772348, + "valid": true + }, + { + "chainage_m": 340.0, + "x": 208539.146978, + "y": 367728.871703, + "z": 870.732001, + "elevation_m": 870.732001, + "valid": true + }, + { + "chainage_m": 341.0, + "x": 208539.594186, + "y": 367727.977273, + "z": 870.954294, + "elevation_m": 870.954294, + "valid": true + }, + { + "chainage_m": 342.0, + "x": 208540.041395, + "y": 367727.082844, + "z": 871.692873, + "elevation_m": 871.692873, + "valid": true + }, + { + "chainage_m": 343.0, + "x": 208540.488603, + "y": 367726.188414, + "z": 871.204913, + "elevation_m": 871.204913, + "valid": true + }, + { + "chainage_m": 344.0, + "x": 208540.935811, + "y": 367725.293984, + "z": 870.361649, + "elevation_m": 870.361649, + "valid": true + }, + { + "chainage_m": 345.0, + "x": 208541.38302, + "y": 367724.399554, + "z": 870.112888, + "elevation_m": 870.112888, + "valid": true + }, + { + "chainage_m": 346.0, + "x": 208541.830228, + "y": 367723.505124, + "z": 870.160481, + "elevation_m": 870.160481, + "valid": true + }, + { + "chainage_m": 347.0, + "x": 208542.277437, + "y": 367722.610695, + "z": 870.356587, + "elevation_m": 870.356587, + "valid": true + }, + { + "chainage_m": 348.0, + "x": 208542.724645, + "y": 367721.716265, + "z": 870.213329, + "elevation_m": 870.213329, + "valid": true + }, + { + "chainage_m": 349.0, + "x": 208543.171853, + "y": 367720.821835, + "z": 869.701063, + "elevation_m": 869.701063, + "valid": true + }, + { + "chainage_m": 350.0, + "x": 208543.619062, + "y": 367719.927405, + "z": 869.28203, + "elevation_m": 869.28203, + "valid": true + }, + { + "chainage_m": 351.0, + "x": 208544.06627, + "y": 367719.032975, + "z": 869.502922, + "elevation_m": 869.502922, + "valid": true + }, + { + "chainage_m": 352.0, + "x": 208544.513478, + "y": 367718.138546, + "z": 869.69136, + "elevation_m": 869.69136, + "valid": true + }, + { + "chainage_m": 353.0, + "x": 208544.960687, + "y": 367717.244116, + "z": 869.508172, + "elevation_m": 869.508172, + "valid": true + }, + { + "chainage_m": 354.0, + "x": 208545.407895, + "y": 367716.349686, + "z": 869.283434, + "elevation_m": 869.283434, + "valid": true + }, + { + "chainage_m": 355.0, + "x": 208545.855103, + "y": 367715.455256, + "z": 869.157154, + "elevation_m": 869.157154, + "valid": true + }, + { + "chainage_m": 356.0, + "x": 208546.302312, + "y": 367714.560826, + "z": 868.784861, + "elevation_m": 868.784861, + "valid": true + }, + { + "chainage_m": 357.0, + "x": 208546.74952, + "y": 367713.666397, + "z": 868.386015, + "elevation_m": 868.386015, + "valid": true + }, + { + "chainage_m": 358.0, + "x": 208547.196729, + "y": 367712.771967, + "z": 867.871342, + "elevation_m": 867.871342, + "valid": true + }, + { + "chainage_m": 359.0, + "x": 208547.643937, + "y": 367711.877537, + "z": 867.712103, + "elevation_m": 867.712103, + "valid": true + }, + { + "chainage_m": 360.0, + "x": 208548.091145, + "y": 367710.983107, + "z": 867.67227, + "elevation_m": 867.67227, + "valid": true + } + ], + "stations": [ + { + "station_id": "station_000000160000", + "chainage_m": 160.0, + "label": "STA.0+160.000", + "kind": "regular", + "center_x": 208393.562479, + "center_y": 367768.040752, + "center_z": 880.274875, + "azimuth_deg": 153.436904, + "plan_radius_m": null, + "curve_outer_side": "right", + "curve_widening_m": 1.3949, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208393.562479, + "y": 367768.040752, + "z": 880.274875 + }, + "tangent_xy": [ + 0.447183081, + -0.894442447 + ], + "left_xy": [ + 0.894442447, + 0.447183081 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": false, + "pavement_grade_pct": 4.13, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000178290", + "chainage_m": 178.29, + "label": "STA.0+178.290", + "kind": "irregular", + "center_x": 208403.595488, + "center_y": 367752.974512, + "center_z": 878.972372, + "azimuth_deg": 135.0, + "plan_radius_m": null, + "curve_outer_side": "right", + "curve_widening_m": 1.7334, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208403.595488, + "y": 367752.974512, + "z": 878.972372 + }, + "tangent_xy": [ + 0.707106781, + -0.707106781 + ], + "left_xy": [ + 0.707106781, + 0.707106781 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "structure": "세월교", + "pavement_suggested": false, + "pavement_grade_pct": 5.95, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000180000", + "chainage_m": 180.0, + "label": "STA.0+180.000", + "kind": "regular", + "center_x": 208404.804641, + "center_y": 367751.765359, + "center_z": 878.960423, + "azimuth_deg": 134.367573, + "plan_radius_m": null, + "curve_outer_side": "right", + "curve_widening_m": 2.1181, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208404.804641, + "y": 367751.765359, + "z": 878.960423 + }, + "tangent_xy": [ + 0.714868541, + -0.699258871 + ], + "left_xy": [ + 0.699258871, + 0.714868541 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": false, + "pavement_grade_pct": 6.15, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000200000", + "chainage_m": 200.0, + "label": "STA.0+200.000", + "kind": "regular", + "center_x": 208423.257573, + "center_y": 367745.974, + "center_z": 877.309908, + "azimuth_deg": 90.0, + "plan_radius_m": null, + "curve_outer_side": "right", + "curve_widening_m": 0.9385, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208423.257573, + "y": 367745.974, + "z": 877.309908 + }, + "tangent_xy": [ + 1.0, + 0.0 + ], + "left_xy": [ + -0.0, + 1.0 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": false, + "pavement_grade_pct": 9.54, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000220000", + "chainage_m": 220.0, + "label": "STA.0+220.000", + "kind": "regular", + "center_x": 208443.257573, + "center_y": 367745.974, + "center_z": 875.077858, + "azimuth_deg": 90.0, + "plan_radius_m": null, + "curve_outer_side": "right", + "curve_widening_m": 1.2363, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208443.257573, + "y": 367745.974, + "z": 875.077858 + }, + "tangent_xy": [ + 1.0, + 0.0 + ], + "left_xy": [ + -0.0, + 1.0 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": true, + "pavement_grade_pct": 14.18, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000240000", + "chainage_m": 240.0, + "label": "STA.0+240.000", + "kind": "regular", + "center_x": 208462.013694, + "center_y": 367751.682847, + "center_z": 871.348918, + "azimuth_deg": 63.434949, + "plan_radius_m": null, + "curve_outer_side": "right", + "curve_widening_m": 0.0151, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208462.013694, + "y": 367751.682847, + "z": 871.348918 + }, + "tangent_xy": [ + 0.894427191, + 0.447213595 + ], + "left_xy": [ + -0.447213595, + 0.894427191 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": true, + "pavement_grade_pct": 29.16, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000260000", + "chainage_m": 260.0, + "label": "STA.0+260.000", + "kind": "regular", + "center_x": 208479.902238, + "center_y": 367760.627119, + "center_z": 863.832762, + "azimuth_deg": 63.434949, + "plan_radius_m": null, + "curve_outer_side": "left", + "curve_widening_m": 0.1973, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208479.902238, + "y": 367760.627119, + "z": 863.832762 + }, + "tangent_xy": [ + 0.894427191, + 0.447213595 + ], + "left_xy": [ + -0.447213595, + 0.894427191 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": true, + "pavement_grade_pct": 39.87, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000263206", + "chainage_m": 263.21, + "label": "STA.0+263.206", + "kind": "irregular", + "center_x": 208482.769771, + "center_y": 367762.060886, + "center_z": 862.691564, + "azimuth_deg": 63.434949, + "plan_radius_m": null, + "curve_outer_side": "left", + "curve_widening_m": 0.9187, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208482.769771, + "y": 367762.060886, + "z": 862.691564 + }, + "tangent_xy": [ + 0.894427191, + 0.447213595 + ], + "left_xy": [ + -0.447213595, + 0.894427191 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "structure": "파형강관 D800", + "pavement_suggested": true, + "pavement_grade_pct": 22.37, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000280000", + "chainage_m": 280.0, + "label": "STA.0+280.000", + "kind": "regular", + "center_x": 208498.743964, + "center_y": 367765.974, + "center_z": 862.784431, + "azimuth_deg": 90.0, + "plan_radius_m": null, + "curve_outer_side": "left", + "curve_widening_m": 1.054, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208498.743964, + "y": 367765.974, + "z": 862.784431 + }, + "tangent_xy": [ + 1.0, + 0.0 + ], + "left_xy": [ + -0.0, + 1.0 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": true, + "pavement_grade_pct": 14.46, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000300000", + "chainage_m": 300.0, + "label": "STA.0+300.000", + "kind": "regular", + "center_x": 208517.944672, + "center_y": 367762.22357, + "center_z": 867.864402, + "azimuth_deg": 122.623368, + "plan_radius_m": 12.0, + "curve_outer_side": "left", + "curve_widening_m": 2.25, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208517.944672, + "y": 367762.22357, + "z": 867.864402 + }, + "tangent_xy": [ + 0.842232593, + -0.539114328 + ], + "left_xy": [ + 0.539114328, + 0.842232593 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": true, + "pavement_grade_pct": 25.84, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000320000", + "chainage_m": 320.0, + "label": "STA.0+320.000", + "kind": "regular", + "center_x": 208530.20281, + "center_y": 367746.760299, + "center_z": 871.872531, + "azimuth_deg": 153.435283, + "plan_radius_m": null, + "curve_outer_side": "left", + "curve_widening_m": 0.8735, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208530.20281, + "y": 367746.760299, + "z": 871.872531 + }, + "tangent_xy": [ + 0.447208374, + -0.894429802 + ], + "left_xy": [ + 0.894429802, + 0.447208374 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": false, + "pavement_grade_pct": 7.89, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000340000", + "chainage_m": 340.0, + "label": "STA.0+340.000", + "kind": "regular", + "center_x": 208539.146978, + "center_y": 367728.871703, + "center_z": 870.732001, + "azimuth_deg": 153.435283, + "plan_radius_m": null, + "curve_outer_side": null, + "curve_widening_m": 0.0, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208539.146978, + "y": 367728.871703, + "z": 870.732001 + }, + "tangent_xy": [ + 0.447208374, + -0.894429802 + ], + "left_xy": [ + 0.894429802, + 0.447208374 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": false, + "pavement_grade_pct": 8.56, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000344122", + "chainage_m": 344.122, + "label": "STA.0+344.122", + "kind": "irregular", + "center_x": 208540.990371, + "center_y": 367725.184864, + "center_z": 870.282781, + "azimuth_deg": 153.435283, + "plan_radius_m": null, + "curve_outer_side": null, + "curve_widening_m": 0.0, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208540.990371, + "y": 367725.184864, + "z": 870.282781 + }, + "tangent_xy": [ + 0.447208374, + -0.894429802 + ], + "left_xy": [ + 0.894429802, + 0.447208374 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "structure": "파형강관 D800", + "pavement_suggested": true, + "pavement_grade_pct": 15.26, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + }, + { + "station_id": "station_000000360000", + "chainage_m": 360.0, + "label": "STA.0+360.000", + "kind": "regular", + "center_x": 208548.091145, + "center_y": 367710.983107, + "center_z": 867.67227, + "azimuth_deg": 153.435283, + "plan_radius_m": null, + "curve_outer_side": null, + "curve_widening_m": 0.0, + "uphill_side": "right", + "frame": { + "origin": { + "x": 208548.091145, + "y": 367710.983107, + "z": 867.67227 + }, + "tangent_xy": [ + 0.447208374, + -0.894429802 + ], + "left_xy": [ + 0.894429802, + 0.447208374 + ], + "up_xyz": [ + 0.0, + 0.0, + 1.0 + ] + }, + "pavement_suggested": false, + "pavement_grade_pct": 9.6, + "pavement_grade_limit_pct": 14.0, + "uphill_side_source": "user" + } + ], + "design_profiles": [ + { + "schema_version": 1, + "id": "design_grade_line", + "name": "계획선", + "basis": "ground_polyline", + "criteria": { + "max_grade_pct": 14.0, + "max_reverse_grade_pct": 5.0, + "min_vertical_radius_m": 100.0, + "min_curve_length_m": 20.0, + "min_tangent_length_m": 20.0, + "vertical_curve_skip_delta_pct": 5.0, + "design_speed_kph": 20, + "terrain_type": "special", + "paved": false, + "main_direction": "auto", + "balance_segment_length_m": null, + "start_elevation_offset_m": 0.0, + "end_elevation_offset_m": 0.0, + "enforce_pipe_clearance": false, + "resolved_main_direction": "descending" + } + } + ], + "profile_alignment": { + "schema_version": 1, + "policy": { + "station_interval_m": 20.0, + "curve_radius_ratio": 0.4, + "default_curve_length_m": 15.0, + "curve_tangent_max_ratio": 0.45, + "curve_skip_legal_exception": false, + "default_curve_radius_m": 8.0, + "balance_tolerance_percent": 10.0, + "edit_step_m": 0.1, + "grade_violation_policy": "warn", + "max_grade_pct": 14.0, + "curve_skip_delta_pct": 5.0, + "paved": false, + "min_plan_radius_m": 0.0, + "hairpin_min_radius_m": 0.0 + }, + "only_explicit_curves": false, + "base_pvi": [ + { + "chainage_m": 160.0, + "elevation_m": 878.698 + }, + { + "chainage_m": 178.29, + "elevation_m": 877.9714 + }, + { + "chainage_m": 180.0, + "elevation_m": 877.925 + }, + { + "chainage_m": 200.0, + "elevation_m": 876.3436 + }, + { + "chainage_m": 220.0, + "elevation_m": 873.5503 + }, + { + "chainage_m": 240.0, + "elevation_m": 870.7218 + }, + { + "chainage_m": 260.0, + "elevation_m": 868.183 + }, + { + "chainage_m": 263.206, + "elevation_m": 867.9361 + }, + { + "chainage_m": 280.0, + "elevation_m": 866.4094 + }, + { + "chainage_m": 300.0, + "elevation_m": 865.7391 + }, + { + "chainage_m": 320.0, + "elevation_m": 866.0486 + }, + { + "chainage_m": 340.0, + "elevation_m": 867.2154 + }, + { + "chainage_m": 344.122, + "elevation_m": 867.2302 + }, + { + "chainage_m": 360.0, + "elevation_m": 867.3354 + } + ], + "edits": { + "station_offsets": { + "263.206": 1.5, + "263.210": 0.9 + }, + "curve_radii": {} + } + } +} \ No newline at end of file diff --git a/resources/tester/helper_233_1_profile_pick.cjs b/resources/tester/helper_233_1_profile_pick.cjs index a328856e2..2d690aab7 100644 --- a/resources/tester/helper_233_1_profile_pick.cjs +++ b/resources/tester/helper_233_1_profile_pick.cjs @@ -67,7 +67,7 @@ let visible = true; const disposers = []; const pick = PP.bindProfilePick( PID, - target, + { profile: target }, { visible: () => visible, onDispose: (fn) => disposers.push(fn) }, { irregular: () => irregular, pipes: () => pipes, sections: () => sections }, ); diff --git a/resources/tester/helper_233_2_view_pick.cjs b/resources/tester/helper_233_2_view_pick.cjs new file mode 100644 index 000000000..95c32d464 --- /dev/null +++ b/resources/tester/helper_233_2_view_pick.cjs @@ -0,0 +1,316 @@ +/* 233-2 3D · 횡단 카드 · [상세] 강조 = 고름 한 칸에서 파생 — 진짜 TS 를 Node 로 돌림. + * ① 3D: `bindProfilePick`(종단 보기 구독) + 진짜 마커(MK3) · 3D 부재 고름(SPK) · three. + * ② 횡단: `bindCrossPick` · `releaseCrossHighlights`(B06_Section_UI_Page_Pick_Sync) + 카드 · 형상 흉내. + * ③ [상세]: `attachDetailSelect` 다시 그림(창 크기 바뀜) 뒤 고른 선 조각 유지. + * 표준출력 JSON 한 벌 · 짝 = test_233_2_view_pick.py. + */ +const fs = require("fs"); +const path = require("path"); +const ROOT = path.join(__dirname, "..", ".."); +const NM = path.join(ROOT, "config", "node_modules"); +const ts = require(path.join(NM, "typescript")); +const THREE = require(path.join(NM, "three", "build", "three.cjs")); + +function loadTs(file, stubs = {}) { + const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, file), "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const localRequire = (request) => { + if (request === "three") return THREE; + if (stubs[request]) return stubs[request]; + throw new Error(`stub 없음: ${request}`); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} + +/* ── 가짜 DOM(라벨 캔버스 · [상세] 값 칸 · 조각) ── */ +const node = (tag) => { + const self = { + tag, + hidden: false, + textContent: "", + attrs: {}, + classes: new Set(), + parentElement: null, + setAttribute: (k, v) => { + self.attrs[k] = String(v); + if (k === "class") + String(v) + .split(" ") + .forEach((c) => self.classes.add(c)); + }, + getAttribute: (k) => self.attrs[k] ?? null, + addEventListener() {}, + classList: { + toggle: (c, on) => (on ? self.classes.add(c) : self.classes.delete(c)), + contains: (c) => self.classes.has(c), + }, + closest: (sel) => (self.classes.has(sel.slice(1)) ? self : null), + getContext: () => null, + }; + return self; +}; +globalThis.document = { + createElement: node, + createElementNS: (_ns, tag) => node(tag), + addEventListener() {}, + removeEventListener() {}, +}; +globalThis.window = { addEventListener() {}, removeEventListener() {} }; + +const store = new Map(); +const DSel = loadTs("A00_Common/design/design_selection.ts", { + "../b_page_state": { + readState: (key, pid) => store.get(`${key}:${pid}`) ?? null, + writeState: (key, value, pid) => store.set(`${key}:${pid}`, value), + }, +}); +const IRR = { + irregularStationId: (id) => `irregular:${id}`, + isPipeStation: (s) => s.origin === "pipe", +}; +const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", { + "../A00_Common/design/design_selection": DSel, + "./B05_Profile_UI_IrregularStations": IRR, +}); +const PP = loadTs("A00_Common/design/design_pick_profile.ts", { + "./design_selection": DSel, + "../../B05_Profile/B05_Profile_UI_Selection": SY, + "../../B05_Profile/B05_Profile_UI_IrregularStations": IRR, +}); +const MK3 = loadTs("B05_Profile/B05_Profile_UI_Markers.ts", { + "@util/common_util_svg": { inferStationInterval: () => 20, stationLabel: (c) => `0+${c}` }, +}); +const SPK = loadTs("B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts"); +const SYNC = loadTs("B06_Section/B06_Section_UI_Page_Pick_Sync.ts", { + "../A00_Common/design/design_selection": DSel, +}); +const CROSS_PAD = Function( + `return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(fs.readFileSync(path.join(ROOT, "B06_Section", "B06_Section_UI_Section_Common.ts"), "utf8"))[1]}`, +)(); +// [상세] 조각 모듈은 helper_209_1 과 같은 꼴로 딸린 모듈까지 진짜로 읽음(카드 공용만 흉내) +const DEEP_STUBS = { + "@ui/ui_template_context_menu": { createMapContextMenu: () => ({}) }, + "./B06_Section_UI_Section_Common": { CROSS_PAD }, +}; +const resolveTs = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base); +function loadDeep(full) { + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const localRequire = (request) => { + if (DEEP_STUBS[request]) return DEEP_STUBS[request]; + if (request.endsWith(".css")) return {}; + if (request.startsWith("@util/")) + return loadDeep(resolveTs(path.join(ROOT, "common_util", request.slice(6)))); + return loadDeep(resolveTs(path.join(path.dirname(full), request))); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} +const DTS = loadDeep(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Detail_Select.ts")); + +const out = {}; +const PID = "p233b"; +const sel = (chainageM, structureId = null, options = {}) => + DSel.selectDesign( + PID, + chainageM === null ? DSel.NO_SELECTION : { chainageM, structureId }, + options, + ); + +/* ── ① 3D ── */ +const bounds = { x: [0, 200], y: [0, 200], z: [0, 10] }; +const scene = new THREE.Scene(); +const markers = MK3.createRouteMarkers(scene, () => bounds); +const STATIONS = [ + ...[100, 120].map((at) => ({ station_id: `s${at}`, chainage_m: at, center_x: at })), + { station_id: "irregular:sign", chainage_m: 127.86, center_x: 127.86 }, +].map((s) => ({ ...s, center_y: 100, center_z: 5, frame: { left_xy: [0, 1] }, kind: "regular" })); +markers.renderStationLines(STATIONS, 3); +let notified = 0; +markers.onStationSelectionChange(() => (notified += 1)); +const corridor = new THREE.Group(); +const member = new THREE.Mesh(new THREE.BoxGeometry(4, 2, 4), new THREE.MeshLambertMaterial()); +member.userData = { chainageM: 160, kind: "pipe", key: "inlet" }; +const wall = new THREE.Mesh(new THREE.BoxGeometry(4, 2, 4), new THREE.MeshLambertMaterial()); +wall.userData = { chainageM: 120, kind: "wall", key: "own" }; +corridor.add(member, wall); +const structurePick = SPK.bindStructurePick({ + canvas: { addEventListener() {}, removeEventListener() {} }, + camera: () => null, + group: () => corridor, + blocked: () => false, +}); +const profile = { station: null, structure: null }; +let b05Visible = true; +const disposers = []; +PP.bindProfilePick( + PID, + { + profile: { + setSelectedStation: (id) => Object.assign(profile, { station: id, structure: null }), + setSelectedStructure: (id) => Object.assign(profile, { station: null, structure: id }), + }, + viewer: { markers, structurePick }, + }, + { visible: () => b05Visible, onDispose: (fn) => disposers.push(fn) }, + { + irregular: () => [{ id: "sign", chainage_m: 127.86, origin: "user" }], + pipes: () => [], + sections: () => STATIONS.filter((s) => s.station_id.startsWith("s")), + }, +); +const bar = (id) => markers.group.children[1].children.find((o) => o.userData.stationId === id); +const pin = () => markers.group.children.find((g) => g.renderOrder === 9); +const lit = (mesh) => mesh.material.emissive.getHex() !== 0; +const view3d = () => ({ + red: STATIONS.map((s) => s.station_id).filter( + (id) => bar(id).material.color.getHexString() === "ef4444", + ), + pin: pin().visible, + member: lit(member), + wall: lit(wall), + profile: { ...profile }, +}); +sel(120, null, { from: "b06" }); // 횡단 카드 120 고름 → 종단 보기도 값을 받음 +out.card120 = view3d(); +sel(160, null, { partKey: "inlet", from: "b06" }); +out.part160 = view3d(); +sel(127.86, "sign", { from: "b05" }); // 목록 표지 — 그 자리 구조물 측점 띠 +out.sign = view3d(); +sel(null); +out.released3d = view3d(); +out.notified = notified; // 칠하기만 — 마커 알림으로 사슬이 다시 돌지 않음 +disposers.forEach((fn) => fn()); + +/* ── ② 횡단 카드 · 범위 · 형상 ── */ +const calls = []; +const sections = [100, 120].map((at) => ({ station_id: `s${at}`, chainage_m: at })); +const revetLit = new Set([120]); +let cardSelected = "s120"; +let crossVisible = true; +let releaseCross; +const deps = { + sections: () => sections, + controls: { + revetOffset: { + highlightFor: (s) => revetLit.has(s.chainage_m), + selectedFor: (s) => revetLit.has(s.chainage_m), + select: (at) => revetLit.delete(at), + }, + ford: { selectedFor: () => false, select() {} }, + box: { selectedFor: () => false, select() {} }, + }, + releasePipe: () => calls.push("releasePipe"), + view: { + focusStation: (id) => { + calls.push(`focus:${id}`); + if (id === null && cardSelected !== null) { + cardSelected = null; + releaseCross(); // 카드 리스너(null) → 풀기 한 길 다시 부름 — 되돌아온 부름은 건너뜀 + } + }, + refreshCards: (list) => calls.push(`refresh:${list.join(",")}`), + selectMark: (id) => calls.push(`mark:${id}`), + }, + reapply: () => calls.push("reapply"), +}; +const crossDisposers = []; +const crossPick = SYNC.bindCrossPick( + PID, + { visible: () => crossVisible, onDispose: (fn) => crossDisposers.push(fn) }, + deps, +); +releaseCross = () => { + crossPick.release(); + sel(null, null, { from: "b06" }); +}; +sel(120, null, { from: "b06" }); +out.ownPick = calls.splice(0); +sel(140, null, { from: "esc" }); +out.otherPick = calls.splice(0); +releaseCross(); // 카드 다시 누름 · 목록 다시 누름 · 보기 오감 +out.release = calls.splice(0); +out.revetAfter = [...revetLit]; +revetLit.add(100); +cardSelected = "s100"; +sel(null, null, { from: "esc" }); // 다른 곳 풀기(Esc) +out.otherRelease = calls.splice(0); +crossVisible = false; +sel(100, null, { from: "esc" }); +out.hidden = calls.splice(0); +crossDisposers.forEach((fn) => fn()); + +/* ── ③ [상세] 선 조각 — 다시 그린 뒤 고름 유지 ── */ +let axes = { + maxOffset: 10, + pixelsPerMeter: 10, + displayMax: 110, + elevationMid: 100, + exaggeration: 1, +}; +const groundM = [ + [ + [-5, 100], + [0, 101], + ], + [ + [0, 101], + [5, 99], + ], +]; +const toPx = ([o, e]) => + [ + CROSS_PAD.left + (axes.maxOffset - o) * axes.pixelsPerMeter, + CROSS_PAD.top + (axes.displayMax - e) * axes.pixelsPerMeter, + ].join(","); +const layerChildren = []; +const layer = { + querySelectorAll: () => [...layerChildren], + append: (hit) => layerChildren.push(hit), +}; +const drawGround = () => { + layerChildren.length = 0; + for (const pts of groundM) { + const el = node("polyline"); + el.tagName = "polyline"; + el.setAttribute("class", "b06-chart__cross-profile"); + el.setAttribute("points", pts.map(toPx).join(" ")); + el.hasAttribute = () => false; + layerChildren.push(el); + } +}; +let click = null; +const cad = { + addEventListener: (_type, fn) => (click = fn), + querySelector: (s) => (s === "g.b06-chart__plot-layer" ? layer : null), +}; +const select = DTS.attachDetailSelect({ + cad, + design: () => undefined, + axes: () => axes, + picked() {}, + commit() {}, +}); +drawGround(); +select.redraw(); +const hit = layerChildren.find((el) => el.classes.has(DTS.PIECE_HIT_CLASS)); +click({ target: hit, shiftKey: false, ctrlKey: false, metaKey: false, stopPropagation() {} }); +out.detailPicked = select.source("length")?.pieces.length ?? 0; +axes = { ...axes, pixelsPerMeter: 23 }; // 창 크기 바뀜 → 다시 그림 +drawGround(); +select.redraw(); +out.detailAfterRedraw = select.source("length")?.pieces.length ?? 0; +groundM.splice(0, 1, [ + [-6, 100], + [0, 102], +]); // 그 조각 자리가 바뀌면 고름도 빠짐 +drawGround(); +select.redraw(); +out.detailAfterChange = select.source("length")?.pieces.length ?? 0; + +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_213_1_hidden_station_pvi.py b/resources/tester/test_213_1_hidden_station_pvi.py index 4e5480c8c..088edaba4 100644 --- a/resources/tester/test_213_1_hidden_station_pvi.py +++ b/resources/tester/test_213_1_hidden_station_pvi.py @@ -5,7 +5,10 @@ 편집 「263.210」과 0.004m 사이 두 꼭짓점(−15003% 계단). 고침: 계획선 공용 함수(파이썬 `snap_to_stations` ↔ TS `snapToStations`)가 측점 0.006m 안의 꼭짓점 · 편집 키를 그 측점으로 모음(측점 위 것이 이김 · 측점 계획고 그대로) · 192 병합도 -옛 글자를 같은 측점으로 봄. 사용자 storage 는 읽기만 — 사본(tmp_path). +옛 글자를 같은 측점으로 봄. +실자료 = 고정 사본(fixtures/b05_213_1_hidden_station_longitudinal.json · +d2245e3b initial_snapshot 종단 160~360m · 213-1 이전 재현 상태 · 234-2) +— 사용자 storage 에 기대지 않음. """ from __future__ import annotations @@ -32,12 +35,9 @@ from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profi from B05_Profile.B05_Profile_Router_Confirm import _merge_irregular_into_longitudinal # noqa: E402 TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" -SOURCE = ( - PROJECT_ROOT - / "storage/1/3/d2245e3b-0468-4a90-ac5c-50b2adcb56e7/B06_Section/longitudinal/longitudinal.json" -) +SOURCE = PROJECT_ROOT / "resources/tester/fixtures/b05_213_1_hidden_station_longitudinal.json" REL = "B06_Section/longitudinal/longitudinal.json" -needs_copy = pytest.mark.skipif(not SOURCE.is_file(), reason="엄상준_검증 storage 없음") +needs_copy = pytest.mark.skipif(not SOURCE.is_file(), reason="213-1 고정 사본 없음") # 합성 — 격자 20m · 관 측점 263.206 → 263.21 반올림 · 끝 꼭짓점 1000.0(끝 측점 999.998) STATIONS = [0.0, 20.0, 40.0, 263.21, 280.0, 300.0, 999.998] diff --git a/resources/tester/test_233_1_profile_pick.py b/resources/tester/test_233_1_profile_pick.py index 3801a57e6..6e48e9181 100644 --- a/resources/tester/test_233_1_profile_pick.py +++ b/resources/tester/test_233_1_profile_pick.py @@ -86,7 +86,7 @@ def test_풂과_구독_끊음(out) -> None: def test_종단_보기_배선() -> None: view = _read("A00_Common", "design", "design_view_profile.ts") - assert "bindProfilePick(activeProjectId, profilePanel, host," in view + assert "bindProfilePick(activeProjectId, { profile: profilePanel, viewer }, host," in view # 폼이 닫히는 길(저장 뒤 · 관 폼으로)은 종단을 끄지 않고 값대로 다시 얹음 assert "profilePanel.setSelectedStructure(structure?.structure_id ?? null)" not in view assert view.count("profilePick.paint()") >= 3 diff --git a/resources/tester/test_233_2_view_pick.py b/resources/tester/test_233_2_view_pick.py new file mode 100644 index 000000000..134687edd --- /dev/null +++ b/resources/tester/test_233_2_view_pick.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +"""233-2 3D(띠 · 핀 · 부재) · 횡단 카드(고름 · --span · 형상 · 포장층) · [상세] 선 조각 = 고름 한 칸에서 파생. + +까닭: 3D 는 화면끼리 손으로 부르는 사슬로만 칠해져 길마다 빠짐(구조물 고름 뒤 옛 띠 남음 · 횡단 카드 +고름은 부재를 안 켬) · 횡단 카드를 풀어도 `--span` · 포장층이 남음(a4) · 풀기 길마다 끄는 범위가 다름 · +[상세] 선 조각은 다시 그릴 때(창 크기 · 카드 다시 세움) 고름을 지워 바로 꺼짐. +고침: 종단 보기 구독이 3D 띠 · 핀 · 부재까지 값으로 칠함(마커 알림 없이) · 횡단 보기 구독(`bindCrossPick`)은 +다른 곳이 바꾼 값을 넘김 적용 길로 다시 세우고 · 풀기는 한 길(`releaseCrossHighlights` — 카드 · 폼 관 · +범위 테두리 · 포장층 · 형상) · [상세] 다시 그림은 같은 자리 조각 고름 유지. +진짜 TS 를 `helper_233_2_view_pick.cjs` 가 Node(three 포함)로 돌린다. +""" + +from __future__ import annotations + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) + +pytestmark = pytest.mark.skipif( + not os.path.isdir(os.path.join(ROOT, "config", "node_modules")), + reason="프론트엔드 의존성 미설치", +) + + +def _read(*parts: str) -> str: + with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle: + return handle.read() + + +@pytest.fixture(scope="module") +def out() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", os.path.join(HERE, "helper_233_2_view_pick.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_3D_횡단_카드_고름은_띠_핀_그_측점_부재(out) -> None: + card = out["card120"] + assert (card["red"], card["pin"], card["wall"], card["member"]) == (["s120"], True, True, False) + assert card["profile"]["station"] == "s120" + + +def test_3D_부재키_고름은_그_부재만_띠는_없음(out) -> None: + part = out["part160"] + assert (part["red"], part["pin"], part["member"], part["wall"]) == ([], False, True, False) + + +def test_3D_구조물_고름은_그_자리_구조물_측점_띠(out) -> None: + assert out["sign"]["red"] == ["irregular:sign"] + assert out["sign"]["profile"]["structure"] == "sign" + + +def test_3D_풂과_알림_없음(out) -> None: + released = out["released3d"] + assert (released["red"], released["pin"], released["member"], released["wall"]) == ( + [], + False, + False, + False, + ) + assert out["notified"] == 0 # 칠하기만 — 마커 알림이 사슬을 다시 돌리지 않음 + + +def test_횡단_제_누름은_되받지_않고_다른_곳_값은_다시_세움(out) -> None: + assert out["ownPick"] == [] + assert out["otherPick"] == ["reapply"] + assert out["hidden"] == [] # 숨은 보기는 안 받음 + + +def test_횡단_풀기_한_길은_범위_테두리_형상까지(out) -> None: + for key in ("release", "otherRelease"): + assert out[key] == ["releasePipe", "focus:null", "mark:null", out[key][-1]] + assert out["release"][-1] == "refresh:120" + assert out["revetAfter"] == [] + + +def test_상세_다시_그려도_같은_자리_조각_고름_유지(out) -> None: + assert out["detailPicked"] == 1 + assert out["detailAfterRedraw"] == 1 + assert out["detailAfterChange"] == 0 + + +def test_배선() -> None: + cross = _read("A00_Common", "design", "design_view_cross.ts") + assert "bindCrossPick(projectId, host," in cross + assert "crossPick.release()" in cross + # 카드 다시 누름 = 풀기 한 길 + assert "releaseCross(); // 카드 다시 누름" in cross + assert len(cross.splitlines()) <= 685 + profile = _read("A00_Common", "design", "design_view_profile.ts") + assert "{ profile: profilePanel, viewer }" in profile + assert len(profile.splitlines()) <= 700 diff --git a/resources/tester/test_item_picker_detail.py b/resources/tester/test_item_picker_detail.py index 91320af14..33eda500d 100644 --- a/resources/tester/test_item_picker_detail.py +++ b/resources/tester/test_item_picker_detail.py @@ -124,7 +124,9 @@ def test_모달은_M02_와_같은_카드_한_벌을_씀(): assert "mountDrawingTemplate(" not in text and "createSpreadsheet(" not in text, name assert cards.count("mountDrawingCard(") == 1 # 도면 미리보기 = 카드 파일에서 뗀 부품 하나(31-4) drawing_card = (M02 / "M02_MasterTemplete_DrawingCard.ts").read_text(encoding="utf-8") - assert drawing_card.count("mountDrawingTemplate(") == 1 + # 카드 도면 = 여러 장 탭 부품 한 번(105-3 · 36fc3d6c3 부터 mountDrawingTemplate 대신 · 234-2) + assert drawing_card.count("mountStructureSheets(") == 1 + assert "mountDrawingTemplate(" not in drawing_card def test_넣을_값_카드는_sub2_파일_한_벌(out):