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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user