From 80db17d4638815b596e114e4b842b486edcac2cf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 19:57:44 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EC=86=8C=EB=8B=A8):=20=EC=A0=9C=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EB=AC=B4=EB=A6=8E=EC=9D=B4=20=EB=90=98=ED=92=80?= =?UTF-8?q?=EC=9D=B4=EB=8F=BC=20=ED=99=94=EB=A9=B4=EC=9D=B4=20=EB=A9=88?= =?UTF-8?q?=EC=B6=94=EB=8D=98=20=EA=B2=83=20=E2=80=94=20=EC=95=9E=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=88=20=EB=95=8C=EB=A7=8C=20=EB=AC=B4=EB=A6=8E?= =?UTF-8?q?=20=EC=9D=B8=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **실사고(2026-09-07)** — 사용자 공용 브라우저에서 **소단을 한 건 놓자 화면이 25분 넘게 멈춤** (렌더러가 1.4코어를 계속 태움). 서버(파이썬)로 같은 측점을 돌리면 0.01초에 끝나 **서버는 멀쩡**했고 화면만 물렸음. **원인** — `cut_profile_points` 의 무릎(암 경계 교차) 가지에서 **제자리 뒤집기**. 암 경계선의 기울기가 **암 경사와 토사 경사 사이**면 · 토사로 바꾸면 다음 걸음이 경계 **아래** → 「다시 암」 · 암으로 바꾸면 다음 걸음이 경계 **위** → 「다시 토사」 가 되고, 이때 보간 비율 `share` 가 0 이라 `knee_dist == dist` — **한 걸음도 안 나감.** `while dist < limit` 이 끝나지 않음. `multi_knee` 는 **소단이 있으면 항상 참**이라 소단이 지뢰였음. **고침 둘**(파이썬·TS **같은 모양**) 1. **앞으로 나아가는 무릎만 인정** — 직전 무릎 자리를 들고 있다가 같은 자리(1e-9 이내)면 뒤집지 않고 한 걸음 감. **첫 무릎은 종전대로 인정**되므로 `multi_knee=False`(소단 없는 경로)는 한 비트도 안 바뀜 — 그쪽은 첫 뒤집기 뒤 가지 자체가 닫힘. 2. **걸음 수 상한 40,000**(정상 최대 4,000의 10배). 넘으면 **조용히 자르지 않고 경고**를 남김 — 조용히 자르면 절토선이 짧아진 채 값이 나가 또 조용히 틀림(25 지적). **시험 5건**(`tmp/tests/test_berm_knee_progress.py`) — 경계 기울기를 두 경사 사이에 둔 재현으로 ① 8초 안에 끝나는지 ② 거리가 뒤로 안 가는지 ③ 소단 없는 경로가 종전 그대로인지 ④ 두 파일이 같은 규칙을 쓰는지 ⑤ **TS 를 실제로 돌려 파이썬과 꼭짓점 수·끝점이 같은지**. 고치기 전에는 ①이 무한 대기였음. 전체 **491 passed · 17 skipped**, `tsc --noEmit` 통과. 버그를 낸 창(25)과 확인하고 진행함 — 겹치는 작업 없음. Co-Authored-By: Claude Opus 5 (1M context) --- common_util/common_util_cross_berm.py | 39 ++++++++++++++++++++++----- common_util/common_util_cross_berm.ts | 36 ++++++++++++++++++++----- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/common_util/common_util_cross_berm.py b/common_util/common_util_cross_berm.py index 9649bbda..4435b560 100644 --- a/common_util/common_util_cross_berm.py +++ b/common_util/common_util_cross_berm.py @@ -20,9 +20,12 @@ 그래서 지식DB 에도 적지 않는다(사용자 지시). """ +import logging import math from typing import Callable, NamedTuple +logger = logging.getLogger(__name__) + # 소단 기본값 — 근거는 위 모듈 설명. BERM_DEFAULT_WIDTH_M = 0.5 BERM_DEFAULT_INTERVAL_M = 3.0 @@ -31,6 +34,10 @@ BERM_DEFAULT_SLOPE_DEG = 0.0 # 사면을 따라 걸어가는 보폭(m)과 최대 거리 — 무릎 탐색이 쓰던 값과 같다. _STEP_M = 0.05 _MAX_REACH_M = 200.0 +# 걸음 수 상한 — 정상 경로의 최대는 200/0.05 = 4,000 이다. 소단은 걸음 없이 거리를 더하므로 +# 여유를 크게 두고 **10배**로 잡는다. 넘으면 조용히 자르지 않고 경고를 남긴다 — 조용히 +# 자르면 절토선이 짧아진 채 값이 나가 또 조용히 틀린다(2026-09-07 25 지적). +_MAX_STEPS = int(_MAX_REACH_M / _STEP_M) * 10 class BermSpec(NamedTuple): @@ -87,7 +94,24 @@ def cut_profile_points( in_soil = rock_boundary_z is None or elevation >= rock_boundary_z(dist) ratio = soil_cut_ratio if (rock_boundary_z is not None and in_soil) else cut_ratio + # ⚠ 제자리 무릎을 막는 자리 — 경계선 기울기가 **암 경사와 토사 경사 사이**면 「토사로 + # 바꾸면 경계 아래, 암으로 바꾸면 경계 위」가 되어 같은 자리에서 영원히 뒤집힌다 + # (보간 비율 `share` 가 0 이라 한 걸음도 안 나간다). 그러면 화면이 통째로 멈춘다 + # (2026-09-07 실사고 — 소단 한 건을 놓자 브라우저가 25분간 안 끝남). 직전 무릎 자리를 + # 들고 있다가 **같은 자리면 뒤집지 않고 한 걸음 나아간다**. + last_knee_dist = float("-inf") + steps = 0 while dist < limit: + steps += 1 + if steps > _MAX_STEPS: + logger.warning( + "절토 사면 걸음이 상한(%d)을 넘어 멈춥니다 — 거리 %.3fm, 경사비 %.3f. " + "제자리 무릎이 남아 있을 수 있습니다.", + _MAX_STEPS, + dist, + ratio, + ) + break rise = _STEP_M / ratio slant = math.hypot(_STEP_M, rise) @@ -119,12 +143,15 @@ def cut_profile_points( share = min(max(share, 0.0), 1.0) knee_dist = dist + _STEP_M * share knee_z = elevation + rise * share - slant_since_berm += math.hypot(knee_dist - dist, knee_z - elevation) - dist, elevation = knee_dist, knee_z - points.append((dist, elevation)) # 무릎 - in_soil = not in_soil - ratio = soil_cut_ratio if in_soil else cut_ratio - continue + # 앞으로 나아가는 무릎만 인정한다(위 ⚠ 참조). 같은 자리면 그냥 한 걸음 간다. + if knee_dist > last_knee_dist + 1e-9: + slant_since_berm += math.hypot(knee_dist - dist, knee_z - elevation) + dist, elevation = knee_dist, knee_z + points.append((dist, elevation)) # 무릎 + in_soil = not in_soil + ratio = soil_cut_ratio if in_soil else cut_ratio + last_knee_dist = knee_dist + continue dist, elevation = next_dist, next_z slant_since_berm += slant diff --git a/common_util/common_util_cross_berm.ts b/common_util/common_util_cross_berm.ts index bc12a09b..a4842290 100644 --- a/common_util/common_util_cross_berm.ts +++ b/common_util/common_util_cross_berm.ts @@ -25,6 +25,10 @@ export const BERM_DEFAULT_SLOPE_DEG = 0.0; /** 사면을 따라 걸어가는 보폭(m)과 최대 거리 — 파이썬 짝과 같은 값. */ const STEP_M = 0.05; const MAX_REACH_M = 200.0; +/** 걸음 수 상한 — 정상 경로의 최대는 200/0.05 = 4,000. 소단은 걸음 없이 거리를 더하므로 + * 여유를 크게 두고 10배로 잡는다. 넘으면 조용히 자르지 않고 콘솔에 알린다. + * 짝: 파이썬 `_MAX_STEPS`. */ +const MAX_STEPS = (MAX_REACH_M / STEP_M) * 10; /** 소단 제원 — 폭(m) · 간격(사면길이 m) · 안쪽 기울기(도). */ export interface BermSpec { @@ -71,7 +75,21 @@ export function cutProfilePoints( let inSoil = rockBoundaryZ === null || elevation >= rockBoundaryZ(dist); let ratio = rockBoundaryZ !== null && inSoil ? soilCutRatio : cutRatio; + // ⚠ 제자리 무릎을 막는 자리 — 경계선 기울기가 **암 경사와 토사 경사 사이**면 「토사로 + // 바꾸면 경계 아래, 암으로 바꾸면 경계 위」가 되어 같은 자리에서 영원히 뒤집힌다(보간 비율 + // `share` 가 0 이라 한 걸음도 안 나간다). 그러면 화면이 통째로 멈춘다(2026-09-07 실사고 — + // 소단 한 건을 놓자 브라우저가 25분간 안 끝남). 직전 무릎 자리를 들고 있다가 **같은 자리면 + // 뒤집지 않고 한 걸음 나아간다**. 짝: 파이썬 `cut_profile_points`. + let lastKneeDist = Number.NEGATIVE_INFINITY; + let steps = 0; while (dist < limit) { + steps += 1; + if (steps > MAX_STEPS) { + console.warn( + `절토 사면 걸음이 상한(${MAX_STEPS})을 넘어 멈춥니다 — 거리 ${dist.toFixed(3)}m, 경사비 ${ratio}.`, + ); + break; + } const rise = STEP_M / ratio; const slant = Math.hypot(STEP_M, rise); @@ -104,13 +122,17 @@ export function cutProfilePoints( share = Math.min(Math.max(share, 0), 1); const kneeDist = dist + STEP_M * share; const kneeZ = elevation + rise * share; - slantSinceBerm += Math.hypot(kneeDist - dist, kneeZ - elevation); - dist = kneeDist; - elevation = kneeZ; - points.push([dist, elevation]); // 무릎 - inSoil = !inSoil; - ratio = inSoil ? soilCutRatio : cutRatio; - continue; + // 앞으로 나아가는 무릎만 인정한다(위 ⚠ 참조). 같은 자리면 그냥 한 걸음 간다. + if (kneeDist > lastKneeDist + 1e-9) { + slantSinceBerm += Math.hypot(kneeDist - dist, kneeZ - elevation); + dist = kneeDist; + elevation = kneeZ; + points.push([dist, elevation]); // 무릎 + inSoil = !inSoil; + ratio = inSoil ? soilCutRatio : cutRatio; + lastKneeDist = kneeDist; + continue; + } } }