From d21b0ec51df089707fc03ebc65d63c05faf5fc05 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 3 Aug 2026 20:35:15 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=A2=85=EB=8B=A8=EA=B3=A1?= =?UTF-8?q?=EC=84=A0=20=EA=B8=B0=EC=A4=80=EC=9D=84=20L=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=ED=95=98=EA=B3=A0=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EA=B3=A1=EC=84=A0=20R=C2=B7L=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 영구저장소를 직접 확인하니 R·L은 정상 저장돼 있었고, 안 보인 원인은 둘이었다. 호가 너무 짧았다 - 배관 정착 선형은 변화점 대수차가 1~3%로 작아 R 기준(250m)에서 L = R × A 가 3~7m. 350m 연장 도면에서 화면상 10~20px이라 직선과 구분되지 않았다. - 곡선 기준을 R에서 L로 바꿨다: pipe_anchor_curve_length_m = 15.0 신설, R = L / A 로 역산(법정 최소반경 하한 유지). 표고 보정 반복마다 대수차가 변하므로 R도 매 회 다시 역산한다. 변화점마다 호 길이가 같아 도면이 고르다. 테이블 곡선 행이 규칙 측점만 순회했다 - buildCurveRows가 20m 격자 측점만 돌아 배관 변화점(84.3m 등)의 R·L 셀이 아예 그려지지 않았다. 격자 밖 변화점 셀을 추가해 선택·하이라이트와 무관하게 기본으로 값이 보이게 했다. 검증: solve 재실행 후 84.30/200.92/275.71m 호 길이 정확히 15.00m, 149.73m만 대수차 25.2%로 최소반경 바닥에 걸려 46.07m(정상). 배관 4곳 계획고=지반고 유지. typecheck·vite build·ruff·B03 테스트 통과. Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_wf2_Route_Engine_Grade_Profile.py | 33 ++++++++++---- .../B05_wf2_Route_UI_Profile_Table.ts | 45 ++++++++++++++++++- B05_wf2_Route/B05_wf2_Route_UI_Style.css | 7 +++ config/config_system.py | 8 ++++ 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/B05_wf2_Route/B05_wf2_Route_Engine_Grade_Profile.py b/B05_wf2_Route/B05_wf2_Route_Engine_Grade_Profile.py index 1de46689..0cd26186 100644 --- a/B05_wf2_Route/B05_wf2_Route_Engine_Grade_Profile.py +++ b/B05_wf2_Route/B05_wf2_Route_Engine_Grade_Profile.py @@ -41,6 +41,7 @@ from B05_wf2_Route.B05_wf2_Route_Engine_Grade_Solver import ( solve_alignment_elevations, station_breakpoints, ) +from config.config_system import FOREST_ROAD_PROFILE_ALIGNMENT ALIGNMENT_PROFILE_ID = "design_grade_line" ALIGNMENT_BASIS = "station_alignment" @@ -133,9 +134,10 @@ def design_pipe_anchored_profile( 대칭 종단곡선은 원래 변화점(꼭짓점)을 지나지 않으므로, 변화점 표고를 반복 보정해 **곡선이 정확히 배관 지반고를 통과**하도록 맞춘다(중앙종거만큼 꼭짓점을 밀어낸다). - 호 반경은 설계 기준의 종단곡선 최소 반경(`options.min_vertical_radius_m`, - config_system 법정 기준 해석값)을 기본으로 각 변화점에 지정하며, 이 값은 편집 - 델타(curve_radii)로 저장돼 사용자가 B05 테이블에서 그대로 고칠 수 있다. + 호는 **길이 L을 기준**으로 잡는다(`pipe_anchor_curve_length_m`). 변화점 사이 대수차 A가 + 작아 R을 고정하면 L = R × A 가 변화점마다 널뛰기 때문이다. R = L / A 로 역산해 저장하며 + 법정 최소반경(`options.min_vertical_radius_m`) 아래로는 내려가지 않게 눌러 준다. + 이 R은 편집 델타(curve_radii)로 저장돼 사용자가 B05 테이블에서 그대로 고칠 수 있다. 기울기 위반은 막지 않고 경고로 남긴다 — 배관 위치가 우선이고 조정은 사용자 몫이다. """ options.validate() @@ -185,18 +187,30 @@ def design_pipe_anchored_profile( target[0] = fixed[0] target[-1] = fixed[1] - # 각 배관 변화점의 호 반경 기본값 — 설계 기준의 종단곡선 최소 반경. - anchor_radii = {chainage_key(a): float(options.min_vertical_radius_m) for a in anchors} + target_length = float(FOREST_ROAD_PROFILE_ALIGNMENT["pipe_anchor_curve_length_m"]) + min_radius = float(options.min_vertical_radius_m) edits = dict(edits or {}) - edits["curve_radii"] = {**anchor_radii, **(edits.get("curve_radii") or {})} + user_radii = dict(edits.get("curve_radii") or {}) + + def radii_for(nodes_z: np.ndarray) -> dict[str, float]: + """현 표고에서의 대수차로 R = L / A 를 역산한다(사용자 지정 R이 있으면 그쪽이 이긴다).""" + spans = base_s[1:] - base_s[:-1] + grades = (nodes_z[1:] - nodes_z[:-1]) / np.where(spans > 0, spans, 1.0) + resolved: dict[str, float] = {} + for index in range(1, len(base_s) - 1): + delta = abs(float(grades[index] - grades[index - 1])) + radius = target_length / delta if delta > 1e-9 else min_radius + resolved[chainage_key(base_s[index])] = max(radius, min_radius) + return {**resolved, **user_radii} # 대칭 종단곡선은 꼭짓점(변화점)을 지나지 않는다 — 곡선이 배관 지반고를 통과하도록 # 변화점 표고를 반복 보정한다(호가 안쪽으로 파고드는 중앙종거만큼 꼭짓점을 밀어낸다). - # 오차는 반복마다 중앙종거의 고차항만 남아 수 회면 mm 아래로 떨어진다. + # 표고가 움직이면 대수차도 변하므로 R도 매 회 다시 역산한다. base_z = target.copy() - radii_for_iter = edits["curve_radii"] + radii = radii_for(base_z) for _ in range(12): - curves, _curve_warnings = build_curves(base_s, base_z, policy, radii_for_iter) + radii = radii_for(base_z) + curves, _curve_warnings = build_curves(base_s, base_z, policy, radii) plan_at = evaluate(base_s, base_z, curves, base_s) error = target - plan_at error[0] = 0.0 @@ -204,6 +218,7 @@ def design_pipe_anchored_profile( if float(np.max(np.abs(error))) < 1e-4: break base_z = base_z + error + edits["curve_radii"] = radii alignment = build_alignment( base_s=base_s, diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts index 956864df..dc52c473 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts @@ -292,7 +292,12 @@ function curveTitle(curve: AlignmentCurve): string { * 곡선이 없는 측점에도 **빈 칸을 만든다**. 변화점에만 셀을 두면 세로 구분선이 띄엄띄엄 * 끊겨 위쪽 측점값 행들과 격자가 맞지 않는다. */ -function buildCurveRows(options: ProfileTableOptions, centers: number[]): HTMLElement[] { +function buildCurveRows( + options: ProfileTableOptions, + centers: number[], + x: (chainage: number) => number, + cellWidth: number, +): HTMLElement[] { const { alignment, onCurveRadiusChange } = options; const lengthRow = createRow( "b05-profile-table__row--curve is-group-start", @@ -339,6 +344,42 @@ function buildCurveRows(options: ProfileTableOptions, centers: number[]): HTMLEl placeCell(radiusRow, centers[index], radiusCell); }); + // 규칙 측점 격자에 없는 변화점(배관 자리처럼 임의 chainage에 승격된 점)의 곡선도 + // **기본으로** 값을 보여 준다 — 격자에만 셀을 두면 배관 변화점의 R·L이 표에서 통째로 + // 빠져 "R이 안 들어간 것"처럼 읽힌다(2026-08-03 사용자 보고). + const stationKeys = new Set(alignment.stations.map((row) => row.chainage_m.toFixed(3))); + alignment.curves.forEach((curve) => { + const key = curve.chainage_m.toFixed(3); + if (stationKeys.has(key)) return; + const lengthCell = element("span", "b05-profile-table__curve is-floating"); + const radiusCell = element("span", "b05-profile-table__curve is-floating"); + radiusCell.append( + curveInput( + curve, + curve.r_m.toFixed(1), + `종단곡선 반경 R (m) +${curveTitle(curve)}`, + (r) => r, + ), + ); + lengthCell.append( + curveInput( + curve, + curve.l_m.toFixed(2), + `종단곡선 길이 L (m) +${curveTitle(curve)}`, + (l) => (curve.l_m > 1e-9 ? (l * curve.r_m) / curve.l_m : null), + ), + ); + [lengthCell, radiusCell].forEach((cell) => { + if (curve.skip_allowed) cell.classList.add("is-optional"); + if (curve.omitted) cell.classList.add("is-omitted"); + cell.style.width = `${cellWidth}px`; + }); + placeCell(lengthRow, x(curve.chainage_m), lengthCell); + placeCell(radiusRow, x(curve.chainage_m), radiusCell); + }); + function curveInput( curve: AlignmentCurve, value: string, @@ -540,7 +581,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { }); table.append(row); }); - table.append(...buildCurveRows(options, centers)); + table.append(...buildCurveRows(options, centers, x, cellWidth)); // 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다. // 규칙 측점은 종점 이동을 반영한 `centers[index]`, 비정규 측점은 `x(chainage)`를 중심으로 쓴다. diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index 2fa07c92..8e0dba4b 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -1146,3 +1146,10 @@ background: color-mix(in srgb, var(--color-primary) 14%, transparent); outline: 1px solid var(--color-primary); } + +/* 규칙 측점 격자 밖 변화점(배관 자리)의 곡선 R·L 셀 — 격자 셀 위에 얹혀 값을 보인다. */ +.b05-profile-table__curve.is-floating { + z-index: 2; + background: var(--color-surface-raised); + outline: 1px solid var(--color-border); +} diff --git a/config/config_system.py b/config/config_system.py index 69b04c5c..b8fe511f 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -554,6 +554,14 @@ FOREST_ROAD_PROFILE_ALIGNMENT = { # 도면상 곡선이 거의 드러나지 않는다. 곡선을 뚜렷하게 보려면 이 비율을 크게 올린다 # (예: 20.0 → R=400m, A=10%일 때 L=40m). "curve_radius_ratio": 0.40, + # **배관 정착 계획선(1차 로직)의 기본 종단곡선 길이 L(m).** + # 이 선형은 변화점 사이 대수차 A가 작아(1~3%가 흔하다) 반경 R을 기준으로 잡으면 + # L = R × A 가 변화점마다 3~50m로 널뛰고, 작은 쪽은 도면에서 직선과 구분되지 않는다 + # (2026-08-03 사용자 지적). 그래서 **L을 기준값으로 고정**하고 R = L / A 로 역산한다. + # 결과적으로 호 길이가 변화점마다 같아 도면이 고르게 읽힌다. + # 인접 직선이 짧아 `curve_tangent_max_ratio`에 걸리면 자동으로 줄고 경고가 남는다. + # 호를 더/덜 길게 보이려면 여기만 고친다. + "pipe_anchor_curve_length_m": 15.0, # 인접 직선 길이 대비 곡선 반쪽이 점유할 수 있는 최대 비율. # 0.45면 짧은 쪽 직선의 55%가 항상 직선으로 남아 좌우 곡선이 겹치지 않는다. "curve_tangent_max_ratio": 0.45,