feat(B05): 구조물 측점과 겹치는 규칙 측점 제거

구조물은 보조말뚝을 박아 그 자리가 정본 측점이 된다. 규칙 격자(20m)와 0.1m
안에서 만나면 세로선·라벨이 두 겹으로 겹쳐 읽히지 않았다.

- 공용 유틸 dropStationsNear 추가(B05_Profile_Util_Station).
- 종단 그래프와 3D 측점 띠가 같은 규칙을 쓴다 — 두 화면의 측점이 어긋나지 않는다.

검증: tsc --noEmit 통과, tmp/tests/test_b05_station_merge.mjs 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 15:04:40 +09:00
co-authored by Claude Opus 5
parent bdbefd9472
commit a360c0caba
3 changed files with 31 additions and 2 deletions
+4 -1
View File
@@ -1,3 +1,4 @@
import { dropStationsNear } from "./B05_Profile_Util_Station";
import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
import { showToast } from "@ui/ui_template_elements";
@@ -449,7 +450,9 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
return best.elevation_m;
};
// 측점 바 양 끝 램프용 상단측: 사용자 변경분 → solve 자동 판정 순으로 적용.
const withUphill = [...regular, ...injected].map((station) => {
// 구조물 측점과 0.1m 안에서 겹치는 규칙 측점은 지운다 — 3D 측점 띠도 종단 그래프와
// 같은 규칙을 써야 두 화면의 측점이 어긋나지 않는다(2026-09-06).
const withUphill = [...dropStationsNear(regular, injected), ...injected].map((station) => {
const design = designAt(station.chainage_m);
return {
...station,
+6 -1
View File
@@ -8,6 +8,7 @@
* 거친다. 그래프·테이블·유토곡선이 같은 X 매핑을 쓰는 규칙은 그대로다.
* ========================================================================== */
import { dropStationsNear } from "./B05_Profile_Util_Station";
import {
createLongitudinalProfile,
longitudinalMinimumWidth,
@@ -237,9 +238,13 @@ export function renderProfile(ctx: ProfileRenderContext): void {
// 그대로 남는다(2026-08-02 사용자 보고).
const regular = graphData.stations.filter((station) => station.kind !== "irregular");
const injected = irregularGraphStations(irregularStations, maxChainageOf(longitudinal));
// 구조물 측점과 0.1m 안에서 겹치는 규칙 측점은 지운다 — 세로선·라벨이 두 겹으로
// 겹쳐 읽히지 않는다(2026-09-06). 남는 쪽은 구조물 측점이다.
const graphLongitudinal = {
...graphData,
stations: [...regular, ...injected].sort((a, b) => a.chainage_m - b.chainage_m),
stations: [...dropStationsNear(regular, injected), ...injected].sort(
(a, b) => a.chainage_m - b.chainage_m,
),
};
// 세로 자동 맞춤 — 지금 화면에 보이는 누가거리 구간만 보고 Y 창을 잡는다(2026-09-04
// 사용자 확정). 가로 스크롤 위치(`scrollLeft`)와 본문 폭이 곧 보이는 구간이다.
+21
View File
@@ -52,3 +52,24 @@ export function parseStationText(text: string, intervalM: number): number | null
const direct = Number(trimmed);
return Number.isFinite(direct) && direct >= 0 ? direct : null;
}
/** 구조물 측점과 겹쳤다고 볼 거리(m) — 이보다 가까우면 규칙 측점을 지운다. */
export const STATION_MERGE_TOLERANCE_M = 0.1;
/**
* 구조물 측점과 **겹치는 규칙 측점을 지운다**(2026-09-06).
*
* 구조물은 보조말뚝을 박아 그 자리가 정본 측점이 된다. 규칙 격자(20m)와 0.1m 안에서
* 만나면 세로선·라벨이 두 겹으로 겹쳐 읽히지 않으므로 구조물 쪽만 남긴다.
*/
export function dropStationsNear<T extends { chainage_m: number }>(
stations: readonly T[],
anchors: ReadonlyArray<{ chainage_m: number }>,
toleranceM: number = STATION_MERGE_TOLERANCE_M,
): T[] {
if (anchors.length === 0) return [...stations];
return stations.filter(
(station) =>
!anchors.some((anchor) => Math.abs(anchor.chainage_m - station.chainage_m) <= toleranceM),
);
}