From b3c64625dfa8487d80744f8505e7c12616933c4b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 3 Aug 2026 21:07:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05):=203D=20=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=EA=B8=B0=EB=B3=B8=20=EC=BC=9C=EC=A7=90,?= =?UTF-8?q?=20BP=C2=B7EP=C2=B75=EC=B8=A1=EC=A0=90=20=EB=B0=B0=EC=88=98?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [측점 라벨] 토글 기본값을 켜짐으로 바꿨다. 3D에서 측점 위치를 바로 읽는 것이 기본 동작이다. - 라벨 대상 확대: BP·EP(이름 접두), 5측점 배수 규칙 측점, 구조물(비정규) 측점 (측점번호 + 구조물 이름). 나머지 규칙 측점은 달지 않는다 — 전부 달면 겹친다. - 표기를 종단 그래프·도면 테이블과 같은 규칙(측점번호+잔여거리)으로 통일했다. 서버 label은 STA.0+000.000 형식이라 화면마다 표기가 갈렸다. 누가거리와 최빈 측점간격으로 되짚어 stationLabel()로 만든다. 확인: 23측점 노선에서 라벨 9개(BP / 5·10·15측점 / 구조물 4 / EP)만 표시됨을 실데이터로 검증. typecheck·vite build 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B05_wf2_Route/B05_wf2_Route_UI_Markers.ts | 57 +++++++++++++++++++---- B05_wf2_Route/B05_wf2_Route_UI_Panel.ts | 4 +- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts b/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts index 7eea5dfb..a7036a98 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts @@ -1,4 +1,5 @@ import * as THREE from "three"; +import { inferStationInterval, stationLabel } from "@util/common_util_svg"; export type RoutePointKind = "bp" | "ep" | "cp" | "ap" | "fp"; @@ -33,14 +34,17 @@ export interface SectionStationMarker { /** 상단측(등고 높은 쪽) — 램프 컬러 표시용. Page가 사용자 변경분을 반영해 넘긴다. */ uphill_side?: "left" | "right" | null; frame: { left_xy: [number, number] }; - /** 측점 종류 — 구조물(비정규) 측점만 3D 라벨을 단다(2026-08-03 사용자 지시). */ + /** 측점 종류(`bp`/`ep`/`regular`/`irregular`) — 어느 측점에 라벨을 달지 가르는 기준. */ kind?: string; - /** 측점번호 표기(`4+0.0`). 라벨 본문에 그대로 쓴다. */ - label?: string; + /** 시점부터의 누가거리(m). 측점번호·라벨 표기를 여기서 되짚는다. */ + chainage_m?: number; /** 구조물 이름(배관 등). 있으면 측점번호 뒤에 붙인다. */ structure?: string; } +/** 규칙 측점 라벨을 몇 칸마다 달지. 전부 달면 글자가 겹쳐 도면을 못 읽는다. */ +const STATION_LABEL_STEP = 5; + // 측점 바 양 끝 원형 램프 색: 상단(등고 높은 쪽) 예상측=주황, 반대측=회색. const UPHILL_LAMP_COLOR = 0xf97316; const UPHILL_LAMP_INACTIVE_COLOR = 0x9ca3af; @@ -89,7 +93,8 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou const stationGroup = new THREE.Group(); // 구조물 측점 라벨(스프라이트) 전용 — 측점선과 별개 토글이라 그룹을 나눈다. const stationLabelGroup = new THREE.Group(); - stationLabelGroup.visible = false; + // 기본 켜짐 — 측점 위치를 3D에서 바로 읽는 것이 기본 동작이다(2026-08-04 사용자 지시). + stationLabelGroup.visible = true; interactionGroup.add(markerGroup, stationGroup, stationLabelGroup); scene.add(interactionGroup, routeGroup); let points = emptyPoints(); @@ -249,6 +254,35 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou }); } + /** + * 이 측점에 라벨을 달지, 단다면 무슨 글자를 쓸지 (2026-08-04 사용자 지시). + * + * BP·EP — 시·종점은 항상. 이름을 앞에 붙여 어느 끝인지 바로 읽히게 한다. + * 구조물(비정규) — 측점번호 + 구조물 이름(배관 등). + * 5측점 배수 — 규칙 측점은 5칸마다만. 전부 달면 글자가 겹쳐 도면을 못 읽는다. + * + * 측점번호는 라벨 표기(`측점번호+잔여거리`)에서 되짚는다 — 측점간격은 렌더러가 모른다. + * 잔여거리가 남은 측점(예: `4+12.3`)은 규칙 격자가 아니므로 배수 판정에서 뺀다. + */ + function stationLabelText(station: SectionStationMarker, intervalM: number): string | null { + const chainage = station.chainage_m; + if (!Number.isFinite(chainage)) return null; + // 표기는 종단 그래프·도면 테이블과 **같은 규칙**(`측점번호+잔여거리`)을 쓴다. + // 서버가 내려주는 `label`(`STA.0+000.000`)을 그대로 쓰면 화면마다 표기가 갈린다. + const text = stationLabel(chainage as number, intervalM); + if (station.kind === "bp") return `BP ${text}`; + if (station.kind === "ep") return `EP ${text}`; + if (station.kind === "irregular") { + const structure = station.structure?.trim(); + return structure ? `${text} ${structure}` : text; + } + const safeInterval = intervalM > 0 ? intervalM : 1; + const stationNumber = Math.round((chainage as number) / safeInterval); + const remainder = (chainage as number) - stationNumber * safeInterval; + if (Math.abs(remainder) > 0.05) return null; + return stationNumber % STATION_LABEL_STEP === 0 ? text : null; + } + /** * 측점 라벨 스프라이트. 카메라를 항상 바라보는 캔버스 텍스처라 줌·회전과 무관하게 읽힌다. * 라벨은 **구조물(비정규) 측점에만** 단다 — 전 측점에 달면 글자가 겹쳐 도면을 못 읽는다. @@ -288,6 +322,12 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou disposeGroup(stationLabelGroup); const bounds = getBounds(); if (!bounds || halfWidth <= 0) return; + // 측점간격은 최빈 간격으로 되짚는다 — 비정규 측점이 섞여 있어도 규칙 간격이 나온다. + const stationIntervalM = inferStationInterval( + stations + .filter((entry) => Number.isFinite(entry.chainage_m)) + .map((entry) => ({ chainage_m: entry.chainage_m as number })), + ); stations.forEach((station) => { if (station.center_z === null) return; const [leftX, leftY] = station.frame.left_xy; @@ -313,12 +353,9 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou // 측점 바 양 끝 원형 램프: 상단(등고 높은 쪽) 예상측 컬러, 반대측 회색. // 클릭하면 그 측을 상단측(=측구 방향)으로 지정한다(onUphillPick). - // 구조물 측점 라벨: `측점번호 구조물명`. 일반 측점은 라벨을 달지 않는다. - if (station.kind === "irregular" && station.label) { - const text = station.structure?.trim() - ? `${station.label} ${station.structure.trim()}` - : station.label; - stationLabelGroup.add(stationLabelSprite(text, modelToScene(center, bounds))); + const labelText = stationLabelText(station, stationIntervalM); + if (labelText) { + stationLabelGroup.add(stationLabelSprite(labelText, modelToScene(center, bounds))); } (["left", "right"] as const).forEach((side, endIndex) => { diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts index b79187d6..27e514d2 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts @@ -52,7 +52,7 @@ interface PanelCallbacks { onContoursVisible: (visible: boolean) => void; onAxesVisible: (visible: boolean) => void; onStationLinesVisible: (visible: boolean) => void; - /** 구조물 측점 번호·이름 라벨 표시 토글(기본 꺼짐). */ + /** 측점 번호·이름 라벨 표시 토글(기본 켜짐 — BP·EP·5측점 배수·구조물). */ onStationLabelsVisible: (visible: boolean) => void; onView: (view: "iso" | "top" | "front" | "side") => void; onResetView: () => void; @@ -161,7 +161,7 @@ export function createRoutePanel(callbacks: PanelCallbacks) { toggleButton("등고선", true, callbacks.onContoursVisible), toggleButton("축 표시", false, callbacks.onAxesVisible), toggleButton(L("B05_Route_Field_StationLines"), true, callbacks.onStationLinesVisible), - toggleButton(L("B05_Route_Field_StationLabels"), false, callbacks.onStationLabelsVisible), + toggleButton(L("B05_Route_Field_StationLabels"), true, callbacks.onStationLabelsVisible), ); const separator1 = document.createElement("span"); separator1.className = "b05-route__view-separator";