feat(B05): 3D 절토 구간 측점 표고 · 전 측점 라벨 거리 솎기
사용자 지적(2026-09-04) — 「절토가 된 경우 측점 표시가 안 따라감」, 「측점 라벨이 5측점 기준으로만 붙음, 전체 라벨이 있으면 좋겠음」. - 측점 막대 표고에서 `max(지반, 계획고)` 제거. 코리도가 켜져 있으면 계획고를 그대로 씀 — 절토는 계획고가 지반보다 아래라 max 로는 막대만 원지반에 떠 있었음 (실측 0측점 3.2m 공중). - 라벨을 규칙 측점 전부에 만들고(`STATION_LABEL_STEP` 제거), 카메라 거리로 솎음 (`LABEL_LOD` 150m 미만 전부 · 400m 미만 2칸 · 그 밖 5칸). 구조물·BP·EP 는 항상. - 뷰어 렌더 루프에서 카메라~시점 거리로 단계를 갱신(단계 불변이면 즉시 반환). 자체검증(공용 브라우저 5173, 용화_LAS) — 절토 3측점·성토 10측점 모두 막대와 코리도 표면 높이차 0.80m(띄움값). 라벨 66개 중 화면맞춤 23 · 중간 39 · 근접 66. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,8 +43,16 @@ export interface SectionStationMarker {
|
||||
structure?: string;
|
||||
}
|
||||
|
||||
/** 규칙 측점 라벨을 몇 칸마다 달지. 전부 달면 글자가 겹쳐 도면을 못 읽는다. */
|
||||
const STATION_LABEL_STEP = 5;
|
||||
/**
|
||||
* 규칙 측점 라벨 솎기 — 라벨은 **전 측점에 만들어 두고** 카메라 거리로 골라 보인다
|
||||
* (2026-09-04 사용자 지시 「전체 라벨이 있으면 좋겠음」). 멀면 글자가 겹치므로
|
||||
* 5칸 → 2칸 → 전부로 단계를 올린다. 경계는 카메라~시점거리(m).
|
||||
*/
|
||||
const LABEL_LOD: ReadonlyArray<{ within: number; step: number }> = [
|
||||
{ within: 150, step: 1 },
|
||||
{ within: 400, step: 2 },
|
||||
{ within: Infinity, step: 5 },
|
||||
];
|
||||
|
||||
// 측점 바 양 끝 원형 램프 색: 상단(등고 높은 쪽) 예상측=주황, 반대측=회색.
|
||||
const UPHILL_LAMP_COLOR = 0xf97316;
|
||||
@@ -293,28 +301,42 @@ export function createRouteMarkers(
|
||||
*
|
||||
* BP·EP — 시·종점은 항상. 이름을 앞에 붙여 어느 끝인지 바로 읽히게 한다.
|
||||
* 구조물(비정규) — 측점번호 + 구조물 이름(배관 등).
|
||||
* 5측점 배수 — 규칙 측점은 5칸마다만. 전부 달면 글자가 겹쳐 도면을 못 읽는다.
|
||||
* 규칙 측점 — 전부 만든다. 몇 개를 보일지는 카메라 거리가 정한다(`LABEL_LOD`).
|
||||
*
|
||||
* 측점번호는 라벨 표기(`측점번호+잔여거리`)에서 되짚는다 — 측점간격은 렌더러가 모른다.
|
||||
* 잔여거리가 남은 측점(예: `4+12.3`)은 규칙 격자가 아니므로 배수 판정에서 뺀다.
|
||||
* 잔여거리가 남은 측점(예: `4+12.3`)은 규칙 격자가 아니므로 솎기 판정에서 뺀다
|
||||
* (`number: null` = 거리와 무관하게 항상 보임).
|
||||
*/
|
||||
function stationLabelText(station: SectionStationMarker, intervalM: number): string | null {
|
||||
function stationLabelText(
|
||||
station: SectionStationMarker,
|
||||
intervalM: number,
|
||||
): { text: string; number: number | null } | 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 === "bp") return { text: `BP ${text}`, number: null };
|
||||
if (station.kind === "ep") return { text: `EP ${text}`, number: null };
|
||||
if (station.kind === "irregular") {
|
||||
const structure = station.structure?.trim();
|
||||
return structure ? `${text} ${structure}` : text;
|
||||
return { text: structure ? `${text} ${structure}` : text, number: null };
|
||||
}
|
||||
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;
|
||||
return { text, number: stationNumber };
|
||||
}
|
||||
|
||||
/** 지금 솎기 단계(몇 칸마다 보일지). 카메라 거리로 바뀐다. */
|
||||
let labelStep = LABEL_LOD[LABEL_LOD.length - 1].step;
|
||||
|
||||
function applyLabelStep(): void {
|
||||
stationLabelGroup.children.forEach((child) => {
|
||||
const number = (child.userData as { stationNumber?: number | null }).stationNumber;
|
||||
child.visible = typeof number !== "number" || number % labelStep === 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,9 +427,11 @@ export function createRouteMarkers(
|
||||
|
||||
// 측점 바 양 끝 원형 램프: 상단(등고 높은 쪽) 예상측 컬러, 반대측 회색.
|
||||
// 클릭하면 그 측을 상단측(=측구 방향)으로 지정한다(onUphillPick).
|
||||
const labelText = stationLabelText(station, stationIntervalM);
|
||||
if (labelText) {
|
||||
stationLabelGroup.add(stationLabelSprite(labelText, modelToScene(center, bounds)));
|
||||
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) => {
|
||||
@@ -435,6 +459,8 @@ export function createRouteMarkers(
|
||||
stationGroup.add(lampHit);
|
||||
});
|
||||
});
|
||||
// 새로 만든 라벨에도 지금 솎기 단계를 그대로 먹인다.
|
||||
applyLabelStep();
|
||||
// 재렌더로 좌표가 갱신됐으니 선택 핀도 그 자리로 다시 놓는다.
|
||||
syncSelectionPin();
|
||||
}
|
||||
@@ -537,6 +563,13 @@ export function createRouteMarkers(
|
||||
setStationLabelsVisible(visible: boolean) {
|
||||
stationLabelGroup.visible = visible;
|
||||
},
|
||||
/** 카메라~시점 거리(m)로 규칙 측점 라벨을 솎는다. 구조물·BP·EP 는 늘 보인다. */
|
||||
updateLabelDetail(distanceM: number) {
|
||||
const step = (LABEL_LOD.find((lod) => distanceM < lod.within) ?? LABEL_LOD[0]).step;
|
||||
if (step === labelStep) return;
|
||||
labelStep = step;
|
||||
applyLabelStep();
|
||||
},
|
||||
onChange(listener: (next: RouteDesignPoints) => void) {
|
||||
changeListener = listener;
|
||||
},
|
||||
|
||||
@@ -423,10 +423,10 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
const design = designAt(station.chainage_m);
|
||||
return {
|
||||
...station,
|
||||
center_z:
|
||||
design !== null && station.center_z !== null
|
||||
? Math.max(station.center_z, design)
|
||||
: station.center_z,
|
||||
// 절토 구간에서는 계획고가 지반보다 **아래**다(2026-09-04 사용자 지적).
|
||||
// max 로 잡으면 코리도가 절취해 내려간 노면을 두고 막대만 원지반에 떠 있다.
|
||||
// 코리도가 켜져 있으면(designAt 이 값을 줌) 계획고를 그대로 쓴다.
|
||||
center_z: design !== null && station.center_z !== null ? design : station.center_z,
|
||||
uphill_side:
|
||||
uphillOverrides.get(uphillKey(station.chainage_m)) ?? station.uphill_side ?? null,
|
||||
};
|
||||
|
||||
@@ -367,6 +367,8 @@ export function createRouteViewer(): RouteViewer {
|
||||
} else {
|
||||
compass.setVisible(false);
|
||||
}
|
||||
// 측점 라벨 솎기 — 가까울수록 촘촘히 보인다(단계가 안 바뀌면 모듈 안에서 걸러낸다).
|
||||
markers.updateLabelDetail(camera.position.distanceTo(controls.target));
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
Reference in New Issue
Block a user