From 782139732b3836776f8a3c93e183f955ef2d0fc6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 4 Sep 2026 07:31:08 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=A2=85=EB=8B=A8=ED=91=9C=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=97=B4=20=EC=83=81=EC=8B=9C=20?= =?UTF-8?q?=ED=91=9C=EA=B8=B0=C2=B7=EC=A7=81=EC=84=A0=ED=99=94=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EA=B3=A1=EC=84=A0=EA=B0=92=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구조물 측점 판정을 문자열 키에서 누가거리 허용 오차 0.1m 비교로 바꿈 — 구조물 목록(85.59)과 계획선 측점(85.595)이 셋째 자리에서 어긋나 구조물 열을 못 알아보고 값을 그대로 펼쳐 이웃 값과 겹쳐 보였음. 이제 고른 때만 값 열 오버레이로 보임 - 대수차 1e-4% 미만이면 직선으로 보고 곡선 L·R 을 빈 칸으로 둠 — 직선화하면 대수차가 0 이 되어 R = L ÷ |대수차| 가 수억 m 로 발산했음 (표·값 열 오버레이 공통) Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_UI_Profile_Table.ts | 71 ++++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Profile_Table.ts b/B05_Profile/B05_Profile_UI_Profile_Table.ts index 347b6678..85edbee0 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Table.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Table.ts @@ -75,6 +75,33 @@ const CELL_PADDING_PX = 2; export const TABLE_TARGET_FONT_PX = 12; /** 곡선 셀이 이웃과 겹칠 때 세로(회전) 표기로 줄이는 셀 폭(px). */ const CURVE_ROTATED_WIDTH_PX = 16; +/** + * 같은 측점으로 볼 누가거리 차이(m). + * + * 구조물 목록의 누가거리(`85.59`)와 계획선 측점의 누가거리(`85.595`)는 소수 셋째 자리에서 + * 어긋난다 — 정본을 만든 경로가 달라 최대 0.05m 벌어진다(`_Profile_Edit` 의 같은 상수 참조). + * 문자열 키로 맞추면 구조물 열을 못 알아봐 값이 그대로 펼쳐졌다(2026-09-04 실측). + */ +const SAME_STATION_TOLERANCE_M = 0.1; + +/** + * 대수차가 이 값보다 작으면 **직선**으로 본다(%). + * + * 직선화하면 그 자리의 좌·우 기울기가 같아져 대수차가 0 이 되고, `L = R × |대수차|` 를 + * 뒤집은 R 은 수억 m 로 튄다. 직선 구간에는 반경이 없으므로 표에는 빈 칸으로 둔다 + * (2026-09-04 사용자 지시). 진짜 완만한 변화점(대수차 0.9% 수준)은 그대로 남는다. + */ +const STRAIGHT_DELTA_PCT = 1e-4; + +/** 직선화로 대수차가 사라진 자리인가 — 표기 대상에서 뺀다. */ +function isStraightCurve(curve: AlignmentCurve): boolean { + return Math.abs(curve.delta_pct) < STRAIGHT_DELTA_PCT; +} + +/** 구조물(비정규) 측점 누가거리 목록에 이 측점이 들어 있는가 — 허용 오차로 본다. */ +function isNearChainage(chainageM: number, list: ReadonlyArray): boolean { + return list.some((entry) => Math.abs(entry - chainageM) < SAME_STATION_TOLERANCE_M); +} /** 주어진 글자 크기로 가장 긴 값을 자르지 않고 담는 데 필요한 셀 폭. */ export function tableCellWidthFor(fontPx: number): number { @@ -301,10 +328,10 @@ function curveTitle(curve: AlignmentCurve): string { function buildCurveRows( options: ProfileTableOptions, centers: number[], - /** 구조물 측점 chainage 키(소수 3자리) — 이 열의 곡선 입력은 기본 표기하지 않는다. */ - structureStationKeys: ReadonlySet, - /** 배관 구조물 chainage 키 — R이 필수라 곡선 L·R은 **항상** 표기한다(2026-08-04 지시). */ - pipeStationKeys: ReadonlySet, + /** 구조물 측점 누가거리 — 이 열의 곡선 입력은 기본 표기하지 않는다. */ + structureChainageList: ReadonlyArray, + /** 배관 구조물 누가거리 — R이 필수라 곡선 L·R은 **항상** 표기한다(2026-08-04 지시). */ + pipeChainageList: ReadonlyArray, /** 기본 글자 크기(px)와 행 높이(px) — 회전 셀의 글자 축소 산식에 쓴다. */ fontPx: number, rowHeightPx: number, @@ -330,10 +357,15 @@ function buildCurveRows( // 그대로 서고, 글자 크기는 행 높이에 맞춰 줄인다(구배 행 is-rotated와 같은 방식). const curveEntries = alignment.stations.map((station, index) => { const key = station.chainage_m.toFixed(3); - const isPipe = pipeStationKeys.has(key); + const isPipe = isNearChainage(station.chainage_m, pipeChainageList); // 구조물 측점 열은 곡선 입력도 기본 표기하지 않는다(빈 칸으로 격자만 유지). // 예외: 배관 측점은 R이 필수 입력이라 곡선 L·R을 항상 보인다. - const curve = structureStationKeys.has(key) && !isPipe ? undefined : curveByChainage.get(key); + const found = + isNearChainage(station.chainage_m, structureChainageList) && !isPipe + ? undefined + : curveByChainage.get(key); + // 직선화된 자리는 반경이 없다 — 빈 칸으로 둔다. + const curve = found && !isStraightCurve(found) ? found : undefined; return { center: centers[index], curve, rotated: false }; }); const withCurve = curveEntries.filter((entry) => entry.curve); @@ -471,7 +503,9 @@ function buildSelectedColumn( const right = rightSeg ? pick(rightSeg).toFixed(digits) : ""; return left && right ? { left, right } : { text: left || right || "" }; }; - const curve = alignment.curves.find((entry) => Math.abs(entry.chainage_m - chainage) < 0.01); + const curveAt = alignment.curves.find((entry) => Math.abs(entry.chainage_m - chainage) < 0.01); + // 값 열 오버레이도 같은 규칙 — 직선화된 자리는 곡선 L·R 을 비운다. + const curve = curveAt && !isStraightCurve(curveAt) ? curveAt : undefined; // 거리: 바로 앞 측점(계획선 측점)까지의 간격. const previous = alignment.stations .filter((row) => row.chainage_m < chainage - 1e-6) @@ -567,15 +601,11 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { // 확정 시 종단 정본에 병합된 구조물 측점은 alignment.stations에 규칙 측점처럼 끼어 있다. // 그 열의 기본값은 표기하지 않는다(2026-08-04 사용자 지시) — 사이드바 구조물 목록의 // chainage와 일치하는 측점이 대상이고, 값은 선택 시 값 열 오버레이(하이라이트)가 보여준다. - const structureStationKeys = new Set( - (options.irregularStations ?? []).map((entry) => entry.chainage_m.toFixed(3)), - ); + const structureChainageList = (options.irregularStations ?? []).map((entry) => entry.chainage_m); // 배관 구조물은 R이 필수라 곡선 행만은 기본 표기 예외다(2026-08-04 사용자 지시). - const pipeStationKeys = new Set( - (options.irregularStations ?? []) - .filter((entry) => isPipeStation(entry)) - .map((entry) => entry.chainage_m.toFixed(3)), - ); + const pipeChainageList = (options.irregularStations ?? []) + .filter((entry) => isPipeStation(entry)) + .map((entry) => entry.chainage_m); const selectedIrregularEntry = options.irregularStations?.find( (entry) => irregularStationId(entry.id) === options.selectedStationId, ); @@ -596,7 +626,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { // 값이 없는 측점(절토고/성토고 중 한쪽)도 빈 칸을 만들어야 세로 구분선이 끊기지 않는다. alignment.stations.forEach((station, stationIndex) => { // 구조물 측점 열은 기본값을 비운다 — 격자(빈 셀)만 남기고 값은 선택 오버레이가 맡는다. - const structural = structureStationKeys.has(station.chainage_m.toFixed(3)); + const structural = isNearChainage(station.chainage_m, structureChainageList); const cell = element( "span", `b05-profile-table__cell${structural ? " is-structure" : ""}`, @@ -607,7 +637,14 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { table.append(row); }); table.append( - ...buildCurveRows(options, centers, structureStationKeys, pipeStationKeys, fontSize, rowHeight), + ...buildCurveRows( + options, + centers, + structureChainageList, + pipeChainageList, + fontSize, + rowHeight, + ), ); // 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다.