From 91f86c5629d082106485bf0545b8c4a5abff624c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 8 Aug 2026 17:50:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(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=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EB=B3=80=ED=99=94=EC=A0=90=EC=97=90=EC=84=9C=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20L=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-08-03(d21b0ec)에 곡선 기준을 R에서 L로 바꿨지만 배관 정착 계획선 산출부 한 곳에만 들어갔다. 공용 곡선 계산기와 정책값은 R 기준(측점간격 20m × curve_radius_ratio 0.40 = 8m)으로 남아, 사용자가 변화점을 새로 추가하면 그 자리만 R=8m가 적용됐다(대수차 3%면 L=0.24m — 도면에서 곡선이 사라진다). 화면 요약도 "기본 R 8.0 m"를 그대로 보여줘 오해를 키웠다. - config: pipe_anchor_curve_length_m -> default_curve_length_m(15.0)로 승격. 배관 정착뿐 아니라 모든 변화점의 1차 기준값이다. - AlignmentPolicy에 default_curve_length_m 추가, 정책 스냅샷에도 실어 보낸다. 옛 R 기준 값(default_curve_radius_m)은 옛 저장분 호환용으로 남긴다. - build_curves(백엔드)와 buildCurves(프론트) 규칙 일원화: 사용자 지정 R > 기본 L > (옛 저장분) 기본 R × 대수차. - 인접 직선이 짧아 목표 L을 못 넣으면 **넣을 수 있는 최대 L**까지만 줄이고 경고를 남긴다(2026-08-08 사용자 지시). 경고 문구도 길이 우선으로 고쳤다. - 옛 저장분(정책에 L 없음)은 양쪽 모두 R 기준을 유지한다 — 서버만 L로 올리면 화면과 다른 곡선이 나온다. 새 기준은 계획선을 다시 산출할 때 적용된다. - 패널 요약 라벨: "기본 R" -> "기본 곡선길이 L"(옛 저장분은 "기본 R(옛 저장분)"). 검증(측점간격 20m, 대수차 3%): 기본 L=15.00m/R=500m, 사용자 R=300 지정 시 L=9.00m/R=300m, 인접 직선 10m면 L=9.00m로 잘리고 경고 1건, 옛 스냅샷은 종전대로 L=0.24m/R=8m. typecheck·ruff format·check 통과. Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_Profile_Engine_Grade_Alignment.py | 43 ++++++++++++++----- .../B05_Profile_Engine_Grade_Profile.py | 4 +- .../B05_Profile_UI_Profile_Alignment.ts | 16 +++++-- B05_Profile/B05_Profile_UI_Profile_Panel.ts | 5 ++- config/config_system.py | 11 ++--- 5 files changed, 57 insertions(+), 22 deletions(-) diff --git a/B05_Profile/B05_Profile_Engine_Grade_Alignment.py b/B05_Profile/B05_Profile_Engine_Grade_Alignment.py index 97118f0a..b76f9be8 100644 --- a/B05_Profile/B05_Profile_Engine_Grade_Alignment.py +++ b/B05_Profile/B05_Profile_Engine_Grade_Alignment.py @@ -38,6 +38,9 @@ class AlignmentPolicy: station_interval_m: float curve_radius_ratio: float + # 기본 종단곡선 길이 L(m). `None`은 **옛 R 기준 저장분**이라는 뜻이다 — 그 계획선은 + # 저장 당시 기하를 그대로 지켜야 하므로 L을 새로 끼워 넣지 않는다. + default_curve_length_m: float | None curve_tangent_max_ratio: float curve_skip_legal_exception: bool balance_tolerance_percent: float @@ -62,6 +65,7 @@ class AlignmentPolicy: return cls( station_interval_m=float(station_interval_m), curve_radius_ratio=float(config["curve_radius_ratio"]), + default_curve_length_m=float(config["default_curve_length_m"]), curve_tangent_max_ratio=float(config["curve_tangent_max_ratio"]), curve_skip_legal_exception=bool(config["curve_skip_legal_exception"]), balance_tolerance_percent=float(config["balance_tolerance_percent"]), @@ -83,6 +87,14 @@ class AlignmentPolicy: curve_radius_ratio=float( payload.get("curve_radius_ratio", config["curve_radius_ratio"]) ), + # 옛 스냅샷에는 L이 없다(R 기준 시절). 그때는 저장 당시 기하를 그대로 지킨다 — + # 여기서 L을 끼워 넣으면 화면(옛 정책이면 R로 계산)과 서버가 다른 곡선을 낸다. + # 새 기준으로 올리려면 계획선을 다시 산출해야 한다. + default_curve_length_m=( + float(payload["default_curve_length_m"]) + if payload.get("default_curve_length_m") + else None + ), curve_tangent_max_ratio=float( payload.get("curve_tangent_max_ratio", config["curve_tangent_max_ratio"]) ), @@ -105,10 +117,11 @@ class AlignmentPolicy: @property def default_curve_radius_m(self) -> float: - """측점간격 기준 기본 종단곡선 반경. + """측점간격 기준 기본 종단곡선 반경 — **옛 R 기준 값(호환용)**. - R을 1차 값으로 고정해야 계획고를 편집해도 곡선 반경이 흔들리지 않는다. - 곡선 길이는 L = R × |대수차| 로 따라 움직인다. + 기준은 2026-08-08부터 길이 L이다(`default_curve_length_m`). 이 값은 옛 저장분을 + 읽는 화면이 계산을 이어갈 수 있도록 정책 스냅샷에 남겨 둘 뿐, 새 곡선 계산은 + L을 먼저 본다. """ return max(1e-6, self.station_interval_m * self.curve_radius_ratio) @@ -117,6 +130,7 @@ class AlignmentPolicy: return { "station_interval_m": self.station_interval_m, "curve_radius_ratio": self.curve_radius_ratio, + "default_curve_length_m": self.default_curve_length_m, "curve_tangent_max_ratio": self.curve_tangent_max_ratio, "curve_skip_legal_exception": self.curve_skip_legal_exception, "default_curve_radius_m": self.default_curve_radius_m, @@ -200,24 +214,33 @@ def build_curves( # 삽입하고 생략 가능 구간이라는 표시만 남긴다(config로 실제 생략 전환 가능). skip_allowed = not policy.paved and abs(delta) <= skip_delta + 1e-12 omitted = skip_allowed and policy.curve_skip_legal_exception + # 기준은 길이 L이다(2026-08-08 사용자 확정). 사용자가 이 변화점 R을 직접 지정했으면 + # 그쪽이 이기고, 아니면 기본 L을 그대로 쓴다 — 대수차가 작아도 호가 사라지지 않는다. requested = overrides.get(key) try: - radius = float(requested) if requested is not None else policy.default_curve_radius_m + radius = float(requested) if requested is not None else None except (TypeError, ValueError): - radius = policy.default_curve_radius_m - if not np.isfinite(radius) or radius <= 0: - radius = policy.default_curve_radius_m - length = radius * abs(delta) + radius = None + if radius is not None and (not np.isfinite(radius) or radius <= 0): + radius = None + if radius is not None: + length = radius * abs(delta) + elif policy.default_curve_length_m: + length = policy.default_curve_length_m + else: + # L이 없는 옛 저장분 — 그 시절 기준(R)으로 계산해 저장 당시 기하를 지킨다. + length = policy.default_curve_radius_m * abs(delta) half_limit = ( min(float(spans[index - 1]), float(spans[index])) * policy.curve_tangent_max_ratio ) + # 인접 직선이 짧으면 넣을 수 있는 **최대 L**까지만 줄인다 — 0으로 죽이지 않는다. half = min(length / 2.0, half_limit) if half <= 1e-9: continue if length / 2.0 - half > 1e-6: warnings.append( - f"chainage {chainage:.1f}m: 인접 직선이 짧아 종단곡선 반경을 " - f"{half * 2.0 / abs(delta):.0f}m(길이 {half * 2.0:.1f}m)로 줄였습니다." + f"chainage {chainage:.1f}m: 인접 직선이 짧아 종단곡선 길이를 " + f"{half * 2.0:.1f}m(반경 {half * 2.0 / abs(delta):.0f}m)로 줄였습니다." ) length = half * 2.0 curves.append( diff --git a/B05_Profile/B05_Profile_Engine_Grade_Profile.py b/B05_Profile/B05_Profile_Engine_Grade_Profile.py index 3e055b22..a29c9e84 100644 --- a/B05_Profile/B05_Profile_Engine_Grade_Profile.py +++ b/B05_Profile/B05_Profile_Engine_Grade_Profile.py @@ -134,7 +134,7 @@ def design_pipe_anchored_profile( 대칭 종단곡선은 원래 변화점(꼭짓점)을 지나지 않으므로, 변화점 표고를 반복 보정해 **곡선이 정확히 배관 지반고를 통과**하도록 맞춘다(중앙종거만큼 꼭짓점을 밀어낸다). - 호는 **길이 L을 기준**으로 잡는다(`pipe_anchor_curve_length_m`). 변화점 사이 대수차 A가 + 호는 **길이 L을 기준**으로 잡는다(`default_curve_length_m`). 변화점 사이 대수차 A가 작아 R을 고정하면 L = R × A 가 변화점마다 널뛰기 때문이다. R = L / A 로 역산해 저장하며 법정 최소반경(`options.min_vertical_radius_m`) 아래로는 내려가지 않게 눌러 준다. 이 R은 편집 델타(curve_radii)로 저장돼 사용자가 B05 테이블에서 그대로 고칠 수 있다. @@ -187,7 +187,7 @@ def design_pipe_anchored_profile( target[0] = fixed[0] target[-1] = fixed[1] - target_length = float(FOREST_ROAD_PROFILE_ALIGNMENT["pipe_anchor_curve_length_m"]) + target_length = float(FOREST_ROAD_PROFILE_ALIGNMENT["default_curve_length_m"]) min_radius = float(options.min_vertical_radius_m) edits = dict(edits or {}) user_radii = dict(edits.get("curve_radii") or {}) diff --git a/B05_Profile/B05_Profile_UI_Profile_Alignment.ts b/B05_Profile/B05_Profile_UI_Profile_Alignment.ts index 36855cf6..170b9cbc 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Alignment.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Alignment.ts @@ -18,6 +18,9 @@ export interface AlignmentPolicy { curve_radius_ratio: number; curve_tangent_max_ratio: number; curve_skip_legal_exception: boolean; + /** 기본 종단곡선 길이 L(m) — 2026-08-08부터 이쪽이 기준이다. 옛 저장분은 없거나 null. */ + default_curve_length_m?: number | null; + /** 옛 R 기준 값. L이 없는 저장분을 되살릴 때만 쓴다. */ default_curve_radius_m: number; balance_tolerance_percent: number; edit_step_m: number; @@ -218,16 +221,21 @@ function buildCurves( const chainage = pvi[index].chainage_m; const key = chainageKey(chainage); const skipAllowed = !policy.paved && Math.abs(delta) <= skipDelta + 1e-12; + // 백엔드 build_curves와 같은 규칙 — 기준은 길이 L이고, 사용자가 지정한 R이 있으면 그쪽. + // 기본 L이 없는 옛 저장분만 R 기준으로 되돌아간다. const requested = curveRadii[key]; - const radius = - Number.isFinite(requested) && requested > 0 ? requested : policy.default_curve_radius_m; - const desired = radius * Math.abs(delta); + const hasRequested = Number.isFinite(requested) && requested > 0; + const fallbackLength = Number.isFinite(policy.default_curve_length_m) + ? (policy.default_curve_length_m as number) + : policy.default_curve_radius_m * Math.abs(delta); + const desired = hasRequested ? requested * Math.abs(delta) : fallbackLength; const halfLimit = Math.min(spanLeft, spanRight) * policy.curve_tangent_max_ratio; + // 인접 직선이 짧으면 넣을 수 있는 최대 L까지만 줄인다 — 0으로 죽이지 않는다. const half = Math.min(desired / 2, halfLimit); if (half <= 1e-9) continue; if (desired / 2 - half > 1e-6) { warnings.push( - `${chainage.toFixed(1)}m: 인접 직선이 짧아 종단곡선 반경을 ${((half * 2) / Math.abs(delta)).toFixed(0)}m로 줄였습니다.`, + `${chainage.toFixed(1)}m: 인접 직선이 짧아 종단곡선 길이를 ${(half * 2).toFixed(1)}m(반경 ${((half * 2) / Math.abs(delta)).toFixed(0)}m)로 줄였습니다.`, ); } const length = half * 2; diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel.ts b/B05_Profile/B05_Profile_UI_Profile_Panel.ts index 62b4875b..5335d0b8 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel.ts @@ -417,7 +417,10 @@ export function createRouteProfilePanel( ], ["변화점", `${alignment.pvi.length} 개`], ["종단곡선", `${alignment.curves.filter((curve) => !curve.omitted).length} 개`], - ["기본 R", `${policy.default_curve_radius_m.toFixed(1)} m`], + // 기준은 길이 L이다. 옛 저장분(L 없음)만 그 시절 기준인 R을 그대로 밝혀 적는다. + Number.isFinite(policy.default_curve_length_m) + ? ["기본 곡선길이 L", `${(policy.default_curve_length_m as number).toFixed(1)} m`] + : ["기본 R(옛 저장분)", `${policy.default_curve_radius_m.toFixed(1)} m`], ]; const editedCount = Object.keys(alignment.edits.station_offsets).length; if (editedCount) entries.push(["편집 측점", `${editedCount} 개`, "edited"]); diff --git a/config/config_system.py b/config/config_system.py index 552f9980..c47f7b14 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -633,14 +633,15 @@ 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(m) — 모든 변화점의 1차 기준값.** + # 임도 계획선은 변화점 사이 대수차 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, + # 인접 직선이 짧아 `curve_tangent_max_ratio`에 걸리면 넣을 수 있는 **최대 L**까지만 + # 줄이고 경고를 남긴다(0으로 죽이지 않는다 — 2026-08-08 사용자 지시). + # 사용자가 그 변화점 R을 직접 지정하면 그쪽이 이긴다. 호를 더/덜 길게 보이려면 여기만 고친다. + "default_curve_length_m": 15.0, # 인접 직선 길이 대비 곡선 반쪽이 점유할 수 있는 최대 비율. # 0.45면 짧은 쪽 직선의 55%가 항상 직선으로 남아 좌우 곡선이 겹치지 않는다. "curve_tangent_max_ratio": 0.45,