feat(b05): 3D 측점 띠 끝 주황 구슬(측구 방향 고르기) · 클릭 · 처리기 지움(176-3)

- 저장 길(세션 uphill → uphill_overrides) · B06 종단 병합 함수 남김

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
This commit is contained in:
2026-10-10 11:53:24 +09:00
co-authored by Claude Opus 5.5
parent de1fc63260
commit c23ba2bd02
4 changed files with 40 additions and 74 deletions
+2 -23
View File
@@ -206,10 +206,9 @@ export async function mountProfileView(
let selectionSyncing = false;
/* ── 측점 상단측(=측구 방향) 사용자 변경분 ─────────────────────────────
* solve가 자동 판정한 uphill_side를 3D 램프 클릭으로 바꾼 값. 세션에 보관했다가
* 저장 시 uphill_overrides로 백엔드/DB(종단 정본)에 병합한다. */
* solve가 자동 판정한 uphill_side를 사용자가 바꾼 값. 세션에 보관했다가
* 저장 시 uphill_overrides로 백엔드/DB(종단 정본)에 병합한다(3D 구슬 입력은 176-3 에서 지움). */
const uphillOverrides = loadUphillOverrides(activeProjectId);
const uphillKey = (chainage: number): string => chainage.toFixed(3);
const { syncIrregularSelection, syncBasinHighlight, selectStationOfPipe } = createSelectionSync({
stations: () => bridge.irregularStations(),
@@ -356,25 +355,6 @@ export async function mountProfileView(
profilePanel.setSelectedStation(stationId);
syncIrregularSelection(stationId);
});
// 3D 램프 클릭 → 그 측을 상단측(측구 방향)으로 지정하고 세션 보관 + 램프 재렌더.
viewer.markers.onUphillPick((stationId, side) => {
if (!currentSectionDetail) return;
const base = currentSectionDetail.longitudinal.stations.find(
(station) => station.station_id === stationId,
);
let chainage = base?.chainage_m;
if (chainage === undefined) {
const prefix = irregularStationId("");
if (stationId.startsWith(prefix)) {
const id = stationId.slice(prefix.length);
chainage = bridge.irregularStations().find((entry) => entry.id === id)?.chainage_m;
}
}
if (chainage === undefined) return;
uphillOverrides.set(uphillKey(chainage), side);
saveUphillOverrides(activeProjectId, uphillOverrides);
renderStationLines(currentSectionDetail);
});
viewer.root.append(panel.viewControls);
function restorePanel(next: RouteLatestResponse): void {
@@ -389,7 +369,6 @@ export async function mountProfileView(
routePoints: latest?.route_points ?? [],
liveSamples: profilePanel.alignmentSamples(),
corridorVisible,
uphillOverrides,
});
viewer.renderStationLines(lines, roadWidths[panel.values().gradeClass] / 2);
}
+2 -47
View File
@@ -32,8 +32,6 @@ export interface SectionStationMarker {
center_x: number;
center_y: number;
center_z: number | null;
/** 상단측(등고 높은 쪽) — 램프 컬러 표시용. Page가 사용자 변경분을 반영해 넘긴다. */
uphill_side?: "left" | "right" | null;
frame: { left_xy: [number, number] };
/** 측점 종류(`bp`/`ep`/`regular`/`irregular`) — 어느 측점에 라벨을 달지 가르는 기준. */
kind?: string;
@@ -54,10 +52,6 @@ const LABEL_LOD: ReadonlyArray<{ within: number; step: number }> = [
{ within: Infinity, step: 5 },
];
// 측점 바 양 끝 원형 램프 색: 상단(등고 높은 쪽) 예상측=주황, 반대측=회색.
const UPHILL_LAMP_COLOR = 0xf97316;
const UPHILL_LAMP_INACTIVE_COLOR = 0x9ca3af;
const COLORS: Record<RoutePointKind, number> = {
bp: 0x10b981,
ep: 0xef4444,
@@ -124,7 +118,6 @@ export function createRouteMarkers(
let changeListener: ((points: RouteDesignPoints) => void) | undefined;
let selectionListener: ((point: PlacedRoutePoint | null) => void) | undefined;
let stationSelectionListener: ((stationId: string | null) => void) | undefined;
let uphillPickListener: ((stationId: string, side: "left" | "right") => void) | undefined;
let selectedStationId: string | null = null;
function allPoints(): PlacedRoutePoint[] {
@@ -423,7 +416,7 @@ export function createRouteMarkers(
.map((entry) => ({ chainage_m: entry.chainage_m as number })),
);
// 측점선(노란 띠)은 도로 반폭의 1.5배로 그린다 — 지형에 묻혀 짧아 보이는 문제
// (2026-08-19 사용자 지시). 측구 방향 램프는 늘어난 선의 **끝단**에 따라붙는다.
// (2026-08-19 사용자 지시).
const barHalfWidth = halfWidth * 1.5;
stations.forEach((station) => {
if (station.center_z === null) return;
@@ -456,39 +449,12 @@ export function createRouteMarkers(
bar.userData.stationId = station.station_id;
stationGroup.add(bar);
// 측점 바 양 끝 원형 램프: 상단(등고 높은 쪽) 예상측 컬러, 반대측 회색.
// 클릭하면 그 측을 상단측(=측구 방향)으로 지정한다(onUphillPick).
const label = stationLabelText(station, stationIntervalM);
if (label) {
const sprite = stationLabelSprite(label.text, modelToScene(center, bounds));
sprite.userData.stationNumber = label.number;
stationLabelGroup.add(sprite);
}
(["left", "right"] as const).forEach((side, endIndex) => {
const active = station.uphill_side === side;
// 램프 절반 축소(1.1→0.55) — 측점 고를 때 가리는 문제(2026-08-06 보고).
const lamp = new THREE.Mesh(
new THREE.SphereGeometry(0.55, 14, 10),
new THREE.MeshBasicMaterial({
color: active ? UPHILL_LAMP_COLOR : UPHILL_LAMP_INACTIVE_COLOR,
}),
);
lamp.position.copy(points[endIndex]);
lamp.position.y += 0.3;
lamp.userData.uphillStationId = station.station_id;
lamp.userData.uphillSide = side;
stationGroup.add(lamp);
// 클릭 판정은 예전 크기 그대로 — 투명 히트 구를 겹쳐 "작게 보여도 잘 눌리게".
const lampHit = new THREE.Mesh(
new THREE.SphereGeometry(1.1, 8, 6),
new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false }),
);
lampHit.position.copy(lamp.position);
lampHit.userData.uphillStationId = station.station_id;
lampHit.userData.uphillSide = side;
stationGroup.add(lampHit);
});
});
// 새로 만든 라벨에도 지금 솎기 단계를 그대로 먹인다.
applyLabelStep();
@@ -527,7 +493,7 @@ export function createRouteMarkers(
function selectStation(stationId: string | null): void {
selectedStationId = stationId;
stationGroup.children.forEach((object) => {
// 측점 띠(stationId 보유 Mesh)만 색을 바꾼다 — 램프(uphill*)는 제외.
// 측점 띠(stationId 보유 Mesh)만 색을 바꾼다.
if (!(object instanceof THREE.Mesh) || typeof object.userData.stationId !== "string") return;
const selected = object.userData.stationId === selectedStationId;
(object.material as THREE.MeshBasicMaterial).color.set(selected ? 0xef4444 : 0xfacc15);
@@ -553,14 +519,6 @@ export function createRouteMarkers(
updateSelected,
deleteSelected,
selectObject(object: THREE.Object3D | undefined) {
// 상단측 램프 클릭: 해당 측을 측구 방향으로 지정(측점 선택보다 우선 판정).
if (typeof object?.userData.uphillStationId === "string") {
uphillPickListener?.(
object.userData.uphillStationId,
object.userData.uphillSide as "left" | "right",
);
return;
}
if (typeof object?.userData.stationId === "string") {
selectStation(object.userData.stationId);
selectionListener?.(null);
@@ -610,9 +568,6 @@ export function createRouteMarkers(
onStationSelectionChange(listener: (stationId: string | null) => void) {
stationSelectionListener = listener;
},
onUphillPick(listener: (stationId: string, side: "left" | "right") => void) {
uphillPickListener = listener;
},
dispose() {
disposeGroup(interactionGroup);
disposeGroup(routeGroup);
@@ -17,8 +17,6 @@ export interface StationLinesInput {
liveSamples: ReadonlyArray<{ chainage_m: number; elevation_m: number }> | null;
/** [예상형상] 켜짐 — 측점 바를 계획고 위로 올린다. */
corridorVisible: boolean;
/** 상단측(측구 방향) 사용자 변경분 — 키는 누가거리 3자리. */
uphillOverrides: Map<string, "left" | "right">;
}
export function stationLinesOf(input: StationLinesInput) {
@@ -60,8 +58,6 @@ export function stationLinesOf(input: StationLinesInput) {
// max 로 잡으면 코리도가 절취해 내려간 노면을 두고 막대만 원지반에 떠 있다.
// 코리도가 켜져 있으면(designAt 이 값을 줌) 계획고를 그대로 쓴다.
center_z: design !== null && station.center_z !== null ? design : station.center_z,
uphill_side:
input.uphillOverrides.get(station.chainage_m.toFixed(3)) ?? station.uphill_side ?? null,
};
});
}
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""PLAN 176-3 — B05 3D 측점 노란 띠 끝 주황 구슬(측구 방향 고르기) · 클릭 · 처리기 지움.
저장 길(세션 uphill → uphill_overrides)과 B06 이 쓰는 종단 병합 함수는 남김.
"""
import os
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
def _read(*parts):
with open(os.path.join(ROOT, *parts), encoding="utf-8") as f:
return f.read()
def test_markers_have_no_lamp():
src = _read("B05_Profile", "B05_Profile_UI_Markers.ts")
for gone in ("uphillStationId", "onUphillPick", "0xf97316", "UPHILL_LAMP", "lampHit"):
assert gone not in src, gone
def test_page_has_no_lamp_handler():
src = _read("A00_Common", "design", "design_view_profile.ts")
assert "onUphillPick" not in src
assert "uphillOverrides" not in _read("B05_Profile", "B05_Profile_UI_Page_Stations.ts")
def test_save_path_and_merge_kept():
# 저장 길(세션 → 저장 몸통)과 B06 이 부르는 종단 병합 함수는 그대로
page = _read("A00_Common", "design", "design_view_profile.ts")
assert "persistUphillOverrides" in page
assert "uphill_overrides" in _read("B05_Profile", "B05_Profile_UI_Page_Actions.ts")
assert "def _merge_uphill_overrides_into_longitudinal" in _read(
"B05_Profile", "B05_Profile_Router_Confirm.py"
)