fix(b06): 횡단 보기 반폭 [적용] = 요청값만 — 교차점 맞춤 폭으로 누를 때마다 재생성하던 옛 줄 지움(173-6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -35,7 +35,6 @@ import {
|
||||
crossSaveParts,
|
||||
type SectionPersistContext,
|
||||
} from "../../B06_Section/B06_Section_UI_Page_Persist";
|
||||
import { maxToeFitHalfWidth } from "../../B06_Section/B06_Section_UI_Cross_Fit";
|
||||
import { readStructurePick } from "../../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||
import { NO_SELECTION, PIPE_MATCH_M, selectDesign } from "./design_selection";
|
||||
import { readPendingPipes } from "../../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||
@@ -256,11 +255,8 @@ export async function mountCrossView(
|
||||
*/
|
||||
async function applyCrossHalfWidth(): Promise<void> {
|
||||
if (!sectionDetail) return;
|
||||
// 교차점이 샘플 밖인 측점이 있으면 그 폭까지 넓힌다(카드가 자동 줌아웃할 지반 확보).
|
||||
const requested = crossHalfWidth();
|
||||
const needed = maxToeFitHalfWidth(sectionDetail.cross_sections);
|
||||
const target = Math.max(requested ?? 0, needed);
|
||||
if (target > 0 && !(await ensureSampledWidth(target))) return;
|
||||
if (requested && !(await ensureSampledWidth(requested))) return;
|
||||
if (!sectionDetail) return;
|
||||
stationControls.applyGlobalWidth(
|
||||
requested,
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
* 교차점이 여전히 화면 밖에 남는다(2026-08-23 사용자 보고).
|
||||
*
|
||||
* 설계선이 지반과 만나기 전에 끝나면(계산 반폭 밖) **추세로 외삽**한다 — 끝단의 사면
|
||||
* 기울기와 지반 기울기 차이로 만나는 자리를 추정하고 1m 올림한다. 그 값이 계산 반폭을
|
||||
* 넘으면 페이지가 백엔드 재생성으로 샘플을 넓힌다.
|
||||
* 기울기와 지반 기울기 차이로 만나는 자리를 추정한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossDesign, CrossSection } from "./B06_Section_Api_Fetch";
|
||||
@@ -25,43 +24,10 @@ const TREND_SPAN_M = 2;
|
||||
/** 외삽 결과의 상한(m) — 발산하는 지형에서 폭이 무한정 커지는 것을 막는다. */
|
||||
const MAX_FIT_HALF_WIDTH_M = 60;
|
||||
|
||||
/**
|
||||
* 절·성토 교차점이 모두 들어오는 표시 반폭(m). 설계선·지반선이 없으면 null.
|
||||
*
|
||||
* 반환값은 1m 단위 올림 — 교차점이 축선에 딱 붙지 않게 한다.
|
||||
*/
|
||||
export function toeFitHalfWidth(section: CrossSection): number | null {
|
||||
const design = section.design;
|
||||
if (!design) return null;
|
||||
const groundAt = groundInterpolator(section.samples);
|
||||
const designAt = designInterpolator(design.design_line);
|
||||
const edges = design.road_edges;
|
||||
if (!groundAt || !designAt || !edges) return null;
|
||||
|
||||
// 설계선이 실제로 정의된 범위 — 그 밖은 외삽 영역이다.
|
||||
const lineOffsets = design.design_line.map((point) => point.offset_m);
|
||||
if (lineOffsets.length < 2) return null;
|
||||
const lineMax = Math.max(...lineOffsets);
|
||||
const lineMin = Math.min(...lineOffsets);
|
||||
// 노면·측구 구간은 건너뛴다 — 계획고가 지반과 우연히 겹쳐도 사면 끝이 아니다.
|
||||
const { protectMax, protectMin } = protectedSpan(design, edges);
|
||||
|
||||
let extent = 0;
|
||||
for (const side of ["left", "right"] as const) {
|
||||
const outward = side === "left" ? 1 : -1;
|
||||
const start = side === "left" ? protectMax : protectMin;
|
||||
const limit = side === "left" ? lineMax : lineMin;
|
||||
const meet = meetOffset(designAt, groundAt, start, limit, outward);
|
||||
extent = Math.max(extent, Math.abs(meet));
|
||||
}
|
||||
if (!(extent > 0)) return null;
|
||||
return Math.min(Math.ceil(extent + 1), MAX_FIT_HALF_WIDTH_M);
|
||||
}
|
||||
|
||||
/**
|
||||
* 절·성토 사면이 원지반과 **실제로 만나는** 좌·우 offset(m). 그 측에서 못 만나면 null.
|
||||
*
|
||||
* `toeFitHalfWidth` 와 같은 교차점을 쓰되 **좌우를 따로** 낸다(2026-09-12 사용자 지시 ②) —
|
||||
* 교차점을 **좌우를 따로** 낸다(2026-09-12 사용자 지시 ②) —
|
||||
* 반폭 하나로 자르면 한쪽만 성토인 측점에서 반대쪽 원지반이 필요보다 넓게 남는다.
|
||||
* **외삽으로 짚은 자리는 안 쓴다**(null) — 지반을 모르는 자리라 거기까지 그으면 없는 지반을
|
||||
* 그리는 셈이 된다.
|
||||
@@ -129,7 +95,7 @@ export function fillSlopeLengths(section: CrossSection): {
|
||||
const outward = side === "left" ? 1 : -1;
|
||||
const start = side === "left" ? protectMax : protectMin;
|
||||
const limit = side === "left" ? Math.max(...lineOffsets) : Math.min(...lineOffsets);
|
||||
// 미교차 측점은 **재지 못한 것**이라 계산 반폭까지의 하한값만 준다 — `toeFitHalfWidth`가
|
||||
// 미교차 측점은 **재지 못한 것**이라 계산 반폭까지의 하한값만 준다 — `meetOffset` 이
|
||||
// 쓰는 추세 외삽은 지반이 사면과 거의 나란한 자리에서 수십 m씩 튀어 길이 표기에는 못 쓴다
|
||||
// (2026-09-03 실측: 740m 측점 좌측 외삽 101.53m, 실제 반폭 20m까지 고저차 7.16m 유지).
|
||||
const meet = meetOffset(designAt, groundAt, start, limit, outward);
|
||||
@@ -328,8 +294,3 @@ function extrapolateMeet(
|
||||
if (!Number.isFinite(extra)) return limit;
|
||||
return limit + outward * Math.min(extra, MAX_FIT_HALF_WIDTH_M);
|
||||
}
|
||||
|
||||
/** 전 측점 중 교차점을 보려면 필요한 최대 반폭(m). 없으면 0. */
|
||||
export function maxToeFitHalfWidth(sections: CrossSection[]): number {
|
||||
return sections.reduce((max, section) => Math.max(max, toeFitHalfWidth(section) ?? 0), 0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/* 횡단 보기 반폭 [적용](PLAN 173-6) 단위검증 헬퍼 — `createSampleWidener` 를 그 자리에서 트랜스파일해 돌린다.
|
||||
* 보유 샘플(20 m) 안 요청은 재생성 없이 true · 넘는 요청만 재생성 한 번. 결과는 표준출력에 JSON 한 벌.
|
||||
* 짝 = `test_173_6_half_width_apply.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const calls = { regenerate: [], overlay: 0 };
|
||||
const STUBS = {
|
||||
"@ui/ui_template_locale": {
|
||||
currentLanguageIndex: 0,
|
||||
ui_locales: new Proxy({}, { get: () => [""] }),
|
||||
},
|
||||
"@ui/ui_template_elements": {
|
||||
showLoadingOverlay: () => calls.overlay++,
|
||||
hideLoadingOverlay: () => {},
|
||||
showToast: () => {},
|
||||
},
|
||||
"./B06_Section_Api_Fetch": {
|
||||
regenerateSections: async (_p, _r, width) => calls.regenerate.push(width),
|
||||
fetchSectionDetail: async () => ({ cross_sections: [] }),
|
||||
},
|
||||
"./B06_Section_Section_Store": { replaceSectionDetail: () => {} },
|
||||
};
|
||||
|
||||
const file = path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Common.ts");
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (r) => STUBS[r]);
|
||||
|
||||
(async () => {
|
||||
let sampled = 20;
|
||||
const widen = box.exports.createSampleWidener({
|
||||
target: () => ({ projectId: "p", routeId: 1 }),
|
||||
sampledHalfWidth: () => sampled,
|
||||
applyDetail: () => (sampled = calls.regenerate.at(-1)),
|
||||
});
|
||||
const out = {};
|
||||
out.within = [await widen(10), await widen(15), await widen(20)];
|
||||
out.withinCalls = calls.regenerate.length;
|
||||
out.over = await widen(25);
|
||||
out.overCalls = [...calls.regenerate];
|
||||
out.again = await widen(25); // 넓힌 뒤 같은 값은 재생성 없음
|
||||
out.againCalls = calls.regenerate.length;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})();
|
||||
@@ -0,0 +1,48 @@
|
||||
"""173-6 횡단 보기 반폭 [적용] — 요청값만으로 샘플 폭을 잼(교차점 맞춤 폭 안 씀).
|
||||
|
||||
원지반 못 만나는 측점이 있으면 누를 때마다 서버 재생성하던 옛 줄(`maxToeFitHalfWidth`)을 지움 →
|
||||
보유 샘플(20 m) 넘는 값일 때만 재생성 · 나머지는 카드 다시 그리기만.
|
||||
소스(`design_view_cross.ts` · `B06_Section_UI_Cross_Fit.ts`) + `createSampleWidener` 를 돌린 값
|
||||
(`helper_173_6_half_width.cjs`).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parents[1]
|
||||
DVC = (ROOT / "A00_Common" / "design" / "design_view_cross.ts").read_text(encoding="utf-8")
|
||||
FIT = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Fit.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _apply_body() -> str:
|
||||
at = DVC.index("async function applyCrossHalfWidth")
|
||||
return DVC[at : DVC.index("\n }\n", at)]
|
||||
|
||||
|
||||
def test_적용은_요청값만_넘김() -> None:
|
||||
body = _apply_body()
|
||||
assert "ensureSampledWidth(requested)" in body
|
||||
assert "maxToeFitHalfWidth" not in DVC and "Math.max" not in body
|
||||
|
||||
|
||||
def test_교차점_맞춤_폭_함수_지움() -> None:
|
||||
assert "maxToeFitHalfWidth" not in FIT and "toeFitHalfWidth" not in FIT
|
||||
|
||||
|
||||
@pytest.mark.skipif(not (ROOT / "config" / "node_modules").is_dir(), reason="의존성 미설치")
|
||||
def test_보유_폭_안은_재생성_없음_넘을_때만_한_번() -> None:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_173_6_half_width.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
out = json.loads(done.stdout)
|
||||
assert out["within"] == [True, True, True] and out["withinCalls"] == 0
|
||||
assert out["over"] is True and out["overCalls"] == [25]
|
||||
assert out["again"] is True and out["againCalls"] == 1
|
||||
Reference in New Issue
Block a user