diff --git a/B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts b/B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts index eaee72c90..a9edbc674 100644 --- a/B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts +++ b/B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts @@ -554,7 +554,7 @@ const FEEDBACK_TOLERANCE_M = 1e-4; * 변화점이 새로 생기면 종단곡선의 중앙종거만큼 계획고가 함께 내려가(또는 올라가) * 버튼 1클릭이 정확히 0.1m가 되지 않는다. **화면에 보이는 계획고**가 정확히 delta만큼 * 움직이도록 중앙종거 변화분을 몇 번 되먹여 보정한다. - * 누른 점이 꺾임이면 이웃 꺾임(`pivotAxes`)을 축으로 사이 측점이 새 직선 위로 따라 돈다(355-1). + * 누른 점 좌우 다음 꺾임(`pivotAxes`)을 축으로 사이 측점이 새 직선 위로 따라 돈다(355-1) · 직선 안 점도 그 점이 새 꺾임(368-1). */ export function adjustStation( base: AlignmentBase, diff --git a/B05_Profile/logic/B05_Profile_UI_Profile_Pivot.ts b/B05_Profile/logic/B05_Profile_UI_Profile_Pivot.ts index ccc84efc4..6afe531e8 100644 --- a/B05_Profile/logic/B05_Profile_UI_Profile_Pivot.ts +++ b/B05_Profile/logic/B05_Profile_UI_Profile_Pivot.ts @@ -1,8 +1,9 @@ /* ============================================================================= * B05_Profile_UI_Profile_Pivot.ts - * 종단 ▲▼ 축 찾기(355-1 · 요구 235-15) — 누른 점이 꺾임(R 있는 점)이면 좌우로 같은 - * 기울기 마디를 건너 다음 꺾임(없으면 시점 · 종점)을 축으로 삼는다. 사이 측점은 - * 새 직선(축 → 누른 점 → 축) 위로 따라 돈다(`adjustStation`). + * 종단 ▲▼ 축 찾기(355-1 · 368-1 · 요구 235-15 · 240-20) — 누른 점에서 좌우로 같은 + * 기울기 마디를 건너 다음 꺾임(R 있는 점 · 없으면 시점 · 종점)을 축으로 삼는다. 사이 + * 측점은 새 직선(축 → 누른 점 → 축) 위로 따라 돈다(`adjustStation`). 직선 안 점 · + * 꼭짓점 아닌 자리(추가 측점)도 같은 규칙 — 누른 점이 새 꺾임(기본 L 곡선)이 된다. * * 처음 계획선은 마디마다 꺾여 축 = 누른 점 ±1 → 사이 측점 없음(옛 동작 그대로). * `B05_Profile_UI_Profile_Alignment` 를 타입으로만 읽는다(순환 import 없음). @@ -23,21 +24,23 @@ export interface PivotAxes { inner: number[]; } -/** 누른 점이 꺾임이면 두 축과 사이 변화점 — 직선 안쪽 · 시종점 · 변화점 아님이면 null. */ +/** 누른 점의 두 축과 사이 변화점 — 시종점 · 계획선 밖이면 null. */ export function pivotAxes(alignment: ProfileAlignment, chainageM: number): PivotAxes | null { const { pvi, segments } = alignment; const index = pvi.findIndex((node) => Math.abs(node.chainage_m - chainageM) < SAME_STATION_M); - if (index <= 0 || index >= pvi.length - 1) return null; + // 꼭짓점 아닌 자리 = 그 점을 품은 마디 하나가 좌우 마디 + const right = index >= 0 ? index : pvi.findIndex((node) => node.chainage_m > chainageM); + if (right <= 0 || (index >= 0 && index >= pvi.length - 1)) return null; + const [leftSeg, rightSeg] = index >= 0 ? [index - 1, index] : [right - 1, right - 1]; const grade = (at: number): number => { const segment = segments[at]; return segment && segment.length_m > 1e-12 ? segment.height_m / segment.length_m : 0; }; const same = (a: number, b: number): boolean => Math.abs(grade(a) - grade(b)) < COLLINEAR_EPSILON; - if (same(index - 1, index)) return null; - let from = index - 1; - while (from > 0 && same(from - 1, index - 1)) from -= 1; - let to = index + 1; - while (to < pvi.length - 1 && same(to, index)) to += 1; + let from = leftSeg; + while (from > 0 && same(from - 1, leftSeg)) from -= 1; + let to = rightSeg + 1; + while (to < pvi.length - 1 && same(to, rightSeg)) to += 1; return { fromM: pvi[from].chainage_m, fromZ: pvi[from].elevation_m, diff --git a/resources/tester/helper_355_1_pivot_tilt.cjs b/resources/tester/helper_355_1_pivot_tilt.cjs index 9afd9741f..206f98170 100644 --- a/resources/tester/helper_355_1_pivot_tilt.cjs +++ b/resources/tester/helper_355_1_pivot_tilt.cjs @@ -1,7 +1,8 @@ /* 355-1 종단 ▲▼ 이웃 꺾임 축 돌림 단위검증 헬퍼 — `B05_Profile_UI_Profile_Alignment.ts` · * `_Straighten.ts` 를 그 자리에서 트랜스파일해 세 경우를 Node 로 돌린다. * ① 처음 계획선 ▲ = 옛 값 ② 직선화 뒤 꺾임 ▲ = 사이 측점 한 직선 · 축 표고 그대로 · +0.1 - * ③ 직선 안쪽 ▲ = 옛 값. 옛 값 = 355-1 앞 adjustStation(아래 oldAdjust) 그대로. + * ③ 직선 안 측점 ▲ = 그 점 새 꺾임 · 축 = 좌우 다음 꺾임(368-1) ④ 꼭짓점 아닌 자리 ▲ = 같은 규칙. + * 옛 값 = 355-1 앞 adjustStation(아래 oldAdjust) 그대로. */ const fs = require("fs"); const path = require("path"); @@ -132,19 +133,41 @@ const out = {}; }; } -// ③ 직선 안쪽(80 · 40~100 직선 위) — 꺾임 아님 → 옛 값. -{ +/** 직선 안 점 ▲ — 축 40 · 100 그대로 · 사이 점 새 직선 위 · 누른 점 꺾임(368-1). */ +function tiltInside(pressM, innerS) { let edits = straight.straightenBetween(base, align.emptyEdits(), 40, 100); edits = straight.straightenBetween(base, edits, 100, 160); - const next = align.adjustStation(base, edits, 80, 0.1); - out.inside = { - same: - JSON.stringify(sorted(next.station_offsets)) === - JSON.stringify(sorted(oldAdjust(edits, 80, 0.1).station_offsets)), - planRise: - align.planElevationAt(align.buildAlignment(base, next), 80) - - align.planElevationAt(align.buildAlignment(base, edits), 80), + const before = align.buildAlignment(base, edits); + const next = align.adjustStation(base, edits, pressM, 0.1); + const after = align.buildAlignment(base, next); + const p = pviZ(after, pressM); + const z40 = pviZ(before, 40); + const z100 = pviZ(before, 100); + const line = (s) => + s < pressM + ? z40 + ((p - z40) * (s - 40)) / (pressM - 40) + : z100 + ((p - z100) * (s - 100)) / (pressM - 100); + return { + planRise: align.planElevationAt(after, pressM) - align.planElevationAt(before, pressM), + axisMove: [pviZ(after, 40) - z40, pviZ(after, 100) - z100], + outsideMove: [20, 120, 140, 160].map((s) => pviZ(after, s) - pviZ(before, s)), + innerResidual: innerS.map((s) => pviZ(after, s) - line(s)), + innerMoved: innerS.map((s) => pviZ(after, s) - pviZ(before, s)), + curveAtPress: after.curves.some((c) => Math.abs(c.chainage_m - pressM) < 1e-6), + pressRadiusKey: Object.prototype.hasOwnProperty.call( + next.curve_radii, + align.chainageKey(pressM), + ), + innerRadiusKeys: innerS.filter((s) => + Object.prototype.hasOwnProperty.call(next.curve_radii, align.chainageKey(s)), + ), }; } +// ③ 직선 안 측점(80 · 40~100 직선 위) — 80 새 꺾임 · 축 40 · 100 · 60 새 직선 위. +out.inside = tiltInside(80, [60]); + +// ④ 꼭짓점 아닌 자리(70) — 같은 규칙 · 60 · 80 새 직선 위. +out.between = tiltInside(70, [60, 80]); + process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_355_1_pivot_tilt.py b/resources/tester/test_355_1_pivot_tilt.py index eb24bcbf5..84fb63679 100644 --- a/resources/tester/test_355_1_pivot_tilt.py +++ b/resources/tester/test_355_1_pivot_tilt.py @@ -55,9 +55,17 @@ def test_꺾임_누르면_사이_측점이_한_직선(out): assert kink["oldWouldBreak"] == [0, 0] -def test_직선_안쪽은_옛_값(out): - assert out["inside"]["same"] - assert out["inside"]["planRise"] == pytest.approx(0.1, abs=1e-4) +@pytest.mark.parametrize("case", ["inside", "between"]) +def test_직선_안_점은_새_꺾임_축은_좌우_다음_꺾임(out, case): + # 368-1: 직선 안 측점(80) · 꼭짓점 아닌 자리(70) 모두 그 점에 R · 축 40 · 100 + got = out[case] + assert got["planRise"] == pytest.approx(0.1, abs=1e-4) + assert got["axisMove"] == [0, 0] + assert got["outsideMove"] == [0, 0, 0, 0] + assert all(abs(value) < 1e-6 for value in got["innerResidual"]) + assert all(value > 0 for value in got["innerMoved"]) + assert got["curveAtPress"] and not got["pressRadiusKey"] + assert got["innerRadiusKeys"] == [] def test_부르는_곳_셋이_같은_함수():