feat(B05): 노선 추천 정리 — 알약 다섯 한 줄 · 종단은 갈래가 정함 · 오른쪽 위 [적용]/[닫기] · 지도 안 옵션 겹창 · 추천 토량에 곡선부 확폭(77)
- 알약 = 예상노선 추적(종단 없음) · 영선 · 토공 최소 · 기울기 최소 · 비중 계산(넷은 종단 늘 반영) · 안전 우선 · 계류 횡단 최소 · 공사비 어림 최소 화면에서 뺌 - 「종단 반영」 켬/끔 단추 없앰(추천 창 · 종단 띠) — 띠 · [확인]은 지금 추천 갈래로 종단 여부 - ✕ 없애고 [적용] · [닫기]를 제목줄 오른쪽 위로 · 옵션 칸은 지도 왼쪽 위 작은 겹창 · 창 폭 넓힘 - 적용한 추천 갈래가 있으면 창을 다시 열 때 그 갈래가 골라진 채 - 도움말: 종단 반영 = 측점마다 횡단을 보고 정한 종단(적분값 아님) - 균형 종단 단면적에 곡선부 확폭(B05 측점 생성과 같은 셈) — 빼서 확정 뒤 성토가 1.7배 벌어졌음(노선 267 같은 계획고: 성 10,102 → 17,819 · 본 설계 16,140) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EMfGERKUg5nMMNQsyVZ2j
This commit is contained in:
@@ -228,8 +228,20 @@ const initialRoute = {
|
||||
const mapLines = {
|
||||
fit: model.mapPaths(
|
||||
[
|
||||
{ kind: "expected", points: [[0, 0], [100, 0]] },
|
||||
{ kind: "preview", points: [[0, 0], [100, 50]] },
|
||||
{
|
||||
kind: "expected",
|
||||
points: [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "preview",
|
||||
points: [
|
||||
[0, 0],
|
||||
[100, 50],
|
||||
],
|
||||
},
|
||||
],
|
||||
460,
|
||||
340,
|
||||
@@ -241,7 +253,10 @@ const mapLines = {
|
||||
// ⑦ 되돌리기 · 다시하기가 걸음마다 실은 「현재 추천」을 되살린다(노선과 짝).
|
||||
const history = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_History.ts"));
|
||||
const step = (x, rec) => ({
|
||||
planned: [[x, 0], [x + 1, 1]],
|
||||
planned: [
|
||||
[x, 0],
|
||||
[x + 1, 1],
|
||||
],
|
||||
curveOn: [true, true],
|
||||
curveRadius: [null, null],
|
||||
curveLock: [null, null],
|
||||
@@ -257,7 +272,12 @@ const historyRecs = [trail.undo(), trail.undo(), trail.redo(), trail.redo()].map
|
||||
|
||||
// ⑧ 비중 고정(계획서 26-2c) — 고정한 값은 안 움직이고 나머지가 합 100 을 맞춘다.
|
||||
const W = { expected: 70, earthwork: 15, grade: 15 };
|
||||
const L = (...keys) => ({ expected: false, earthwork: false, grade: false, ...Object.fromEntries(keys.map((k) => [k, true])) });
|
||||
const L = (...keys) => ({
|
||||
expected: false,
|
||||
earthwork: false,
|
||||
grade: false,
|
||||
...Object.fromEntries(keys.map((k) => [k, true])),
|
||||
});
|
||||
const sweep = [];
|
||||
for (const locks of [L(), L("expected"), L("earthwork"), L("grade"), L("expected", "grade")]) {
|
||||
for (const key of ["expected", "earthwork", "grade"]) {
|
||||
@@ -266,7 +286,9 @@ for (const locks of [L(), L("expected"), L("earthwork"), L("grade"), L("expected
|
||||
sweep.push({
|
||||
sum: out.expected + out.earthwork + out.grade,
|
||||
lockedSame: Object.keys(locks).every((k) => !locks[k] || out[k] === W[k]),
|
||||
range: [out.expected, out.earthwork, out.grade].every((n) => Number.isInteger(n) && n >= 0 && n <= 100),
|
||||
range: [out.expected, out.earthwork, out.grade].every(
|
||||
(n) => Number.isInteger(n) && n >= 0 && n <= 100,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -275,9 +297,18 @@ const weightLocks = {
|
||||
freeThree: model.moveWeight(W, L(), "expected", 40),
|
||||
lockExpected: model.moveWeight(W, L("expected"), "earthwork", 20),
|
||||
lockExpectedClamp: model.moveWeight(W, L("expected"), "earthwork", 90),
|
||||
twoLocked: model.moveWeight({ expected: 70, earthwork: 20, grade: 10 }, L("expected", "earthwork"), "grade", 50),
|
||||
twoLocked: model.moveWeight(
|
||||
{ expected: 70, earthwork: 20, grade: 10 },
|
||||
L("expected", "earthwork"),
|
||||
"grade",
|
||||
50,
|
||||
),
|
||||
movingLocked: model.moveWeight(W, L("expected"), "expected", 10),
|
||||
derived: [model.derivedWeightKey(L()), model.derivedWeightKey(L("expected")), model.derivedWeightKey(L("expected", "earthwork"))],
|
||||
derived: [
|
||||
model.derivedWeightKey(L()),
|
||||
model.derivedWeightKey(L("expected")),
|
||||
model.derivedWeightKey(L("expected", "earthwork")),
|
||||
],
|
||||
canLock: {
|
||||
none: model.canLockWeight(L(), "grade"),
|
||||
one: model.canLockWeight(L("expected"), "grade"),
|
||||
@@ -293,8 +324,18 @@ const weightLocks = {
|
||||
over: model.inputWeight(W, L(), "earthwork", 101),
|
||||
nan: model.inputWeight(W, L(), "earthwork", Number.NaN),
|
||||
onLocked: model.inputWeight(W, L("earthwork"), "earthwork", 15),
|
||||
derivedSame: model.inputWeight({ expected: 70, earthwork: 20, grade: 10 }, L("expected", "earthwork"), "grade", 10),
|
||||
derivedOther: model.inputWeight({ expected: 70, earthwork: 20, grade: 10 }, L("expected", "earthwork"), "grade", 30),
|
||||
derivedSame: model.inputWeight(
|
||||
{ expected: 70, earthwork: 20, grade: 10 },
|
||||
L("expected", "earthwork"),
|
||||
"grade",
|
||||
10,
|
||||
),
|
||||
derivedOther: model.inputWeight(
|
||||
{ expected: 70, earthwork: 20, grade: 10 },
|
||||
L("expected", "earthwork"),
|
||||
"grade",
|
||||
30,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -321,10 +362,9 @@ const gradeline = withExtras(
|
||||
);
|
||||
const sixth = {
|
||||
labels: Object.fromEntries(
|
||||
["follow", "gradeline", "earthwork", "gentle", "safe", "crossings", "cost", "weighted"].map((k) => [
|
||||
k,
|
||||
model.LOGIC_LABEL[k],
|
||||
]),
|
||||
["follow", "gradeline", "earthwork", "gentle", "safe", "crossings", "cost", "weighted"].map(
|
||||
(k) => [k, model.LOGIC_LABEL[k]],
|
||||
),
|
||||
),
|
||||
removedLogics: ["short", "balance"].map((k) => k in model.LOGIC_LABEL),
|
||||
// 옛 저장값(short · balance)은 예상노선 추적으로 읽는다 — 서버 기억 · 캐시 둘 다.
|
||||
@@ -337,8 +377,9 @@ const sixth = {
|
||||
criteriaTarget: model.criteriaLine(gradeline.criteria, 0, true),
|
||||
criteriaNoTarget: model.criteriaLine(gradeline.criteria, 0, false),
|
||||
targetParsed: gradeline.criteria.targetGradePct,
|
||||
targetOverride: model.normalizeRouteInitial(plainResponse, "gradeline", null, { target_grade_pct: 10 })
|
||||
.criteriaOverrides,
|
||||
targetOverride: model.normalizeRouteInitial(plainResponse, "gradeline", null, {
|
||||
target_grade_pct: 10,
|
||||
}).criteriaOverrides,
|
||||
targetBase: model.serverBase({ logic: "gradeline", criteria: { target_grade_pct: 10 } }),
|
||||
sameTarget: [
|
||||
model.sameBase(
|
||||
@@ -360,9 +401,21 @@ const sixth = {
|
||||
}),
|
||||
),
|
||||
similarOne: model.similarLine(
|
||||
withExtras({ band_width_m: 13.54, similar: [{ logic: "safe", max_gap_m: 6.1 }] }),
|
||||
withExtras({ band_width_m: 13.54, similar: [{ logic: "gradeline", max_gap_m: 6.1 }] }),
|
||||
),
|
||||
similarNoBand: model.similarLine(
|
||||
withExtras({ similar: [{ logic: "weighted", max_gap_m: 9.1 }] }),
|
||||
),
|
||||
// 화면에서 뺀 갈래(안전 우선 · 계류 횡단 최소 · 공사비 어림 최소)만 있으면 줄 숨김(PLAN 77-1)
|
||||
similarHidden: model.similarLine(
|
||||
withExtras({
|
||||
band_width_m: 13.5,
|
||||
similar: [
|
||||
{ logic: "safe", max_gap_m: 1 },
|
||||
{ logic: "cost", max_gap_m: 2 },
|
||||
],
|
||||
}),
|
||||
),
|
||||
similarNoBand: model.similarLine(withExtras({ similar: [{ logic: "cost", max_gap_m: 9.1 }] })),
|
||||
similarEmpty: model.similarLine(withExtras({ band_width_m: 13.5, similar: [] })),
|
||||
similarMissing: model.similarLine(withExtras({})),
|
||||
similarNull: model.similarLine(null),
|
||||
@@ -464,7 +517,16 @@ const balance63 = {
|
||||
session._map.clear();
|
||||
session.setItem(
|
||||
"aislo:result:routeedit-initial:p-9",
|
||||
JSON.stringify({ logic: "short", weights: null, snapshot: { planned: [[0, 0], [1, 1]] } }),
|
||||
JSON.stringify({
|
||||
logic: "short",
|
||||
weights: null,
|
||||
snapshot: {
|
||||
planned: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
sixth.legacyCache = model.readInitialChoice("p-9");
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""노선 추천 균형 종단에 곡선부 확폭 — PLAN 77-7.
|
||||
|
||||
빼면 확정 뒤 본 설계(확폭 있음)와 성토가 1.7배로 벌어졌다(랩탑 메인 노선 267 실측).
|
||||
추천 측점 확폭 = B05 측점 생성과 같은 `_plan_radii` · `_curve_widenings`.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import _widenings
|
||||
from config.config_system_design import curve_widening_m
|
||||
|
||||
|
||||
def _route() -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
straight = [(x, 0.0) for x in np.arange(0.0, 60.0, 2.0)]
|
||||
radius = 15.0
|
||||
arc = [
|
||||
(60.0 + radius * np.sin(t), radius - radius * np.cos(t))
|
||||
for t in np.linspace(0.0, np.pi / 2, 30)
|
||||
]
|
||||
xy = np.asarray(straight + arc + [(75.0, 15.0 + d) for d in np.arange(2.0, 60.0, 2.0)])
|
||||
chain = np.concatenate([[0.0], np.cumsum(np.hypot(*np.diff(xy, axis=0).T))])
|
||||
at = np.append(np.arange(0.0, chain[-1], 5.0), chain[-1])
|
||||
return xy, chain, at
|
||||
|
||||
|
||||
def test_곡선_측점은_확폭_직선은_없음():
|
||||
xy, chain, at = _route()
|
||||
rows = _widenings(xy, chain, at)
|
||||
assert len(rows) == len(at)
|
||||
on_curve = [row for a, row in zip(at, rows) if 64.0 < a < 80.0]
|
||||
assert on_curve and all(row["curve_widening_m"] > 0 for row in on_curve)
|
||||
assert max(row["curve_widening_m"] for row in on_curve) == curve_widening_m(15.0)
|
||||
assert all(row["curve_outer_side"] in ("left", "right") for row in on_curve)
|
||||
straight = [row for a, row in zip(at, rows) if a < 40.0]
|
||||
assert all(row["curve_widening_m"] == 0 for row in straight)
|
||||
@@ -249,13 +249,13 @@ def test_비중_값_칸_직접_입력(results):
|
||||
}
|
||||
|
||||
|
||||
def test_추천_갈래_여덟_이름_알약_순서(results):
|
||||
def test_추천_갈래_이름_알약_다섯_순서(results):
|
||||
six = results["sixth"]
|
||||
assert six["labels"] == {
|
||||
"follow": "예상노선 추적",
|
||||
"gradeline": "영선 노선",
|
||||
"earthwork": "토공 최소",
|
||||
"gentle": "기울기 순한",
|
||||
"gentle": "기울기 최소",
|
||||
"safe": "안전 우선",
|
||||
"crossings": "계류 횡단 최소",
|
||||
"cost": "공사비 어림 최소",
|
||||
@@ -267,24 +267,17 @@ def test_추천_갈래_여덟_이름_알약_순서(results):
|
||||
with open(page, encoding="utf-8") as handle:
|
||||
text = handle.read()
|
||||
start = text.index("const PILLS")
|
||||
pills = text[start : text.index("];", start)]
|
||||
pills = text[start : text.index("] as const", start)]
|
||||
order = [
|
||||
pills.index(f'"{name}"')
|
||||
for name in (
|
||||
"follow",
|
||||
"gradeline",
|
||||
"earthwork",
|
||||
"gentle",
|
||||
"safe",
|
||||
"crossings",
|
||||
"cost",
|
||||
"weighted",
|
||||
)
|
||||
for name in ("follow", "gradeline", "earthwork", "gentle", "weighted")
|
||||
]
|
||||
assert order == sorted(order) # 알약 순서
|
||||
assert order == sorted(order) # 알약 순서 · 다섯(PLAN 77-1)
|
||||
for gone in ("safe", "crossings", "cost"):
|
||||
assert f'"{gone}"' not in pills
|
||||
assert '"short"' not in pills and '"balance"' not in pills
|
||||
assert (
|
||||
"grid-template-columns: repeat(4"
|
||||
"grid-template-columns: repeat(5"
|
||||
in open(
|
||||
os.path.join(
|
||||
HERE, "..", "..", "B05_Profile", "B05_Profile_UI_Style_RouteRecommend.css"
|
||||
@@ -337,13 +330,14 @@ def test_같은_노선_알림_줄(results):
|
||||
six = results["sixth"]
|
||||
assert six["similarTwo"] == (
|
||||
"이 지형은 제약 안에서 갈 길이 폭 13.5m 한 띠뿐 — "
|
||||
"토공 최소 · 기울기 순한과 거의 같은 노선(가장 먼 거리 6.1m)"
|
||||
"토공 최소 · 기울기 최소와 거의 같은 노선(가장 먼 거리 6.1m)"
|
||||
)
|
||||
assert six["similarOne"] == (
|
||||
"이 지형은 제약 안에서 갈 길이 폭 13.5m 한 띠뿐 — 안전 우선과 거의 같은 노선(가장 먼 거리 6.1m)"
|
||||
"이 지형은 제약 안에서 갈 길이 폭 13.5m 한 띠뿐 — 영선 노선과 거의 같은 노선(가장 먼 거리 6.1m)"
|
||||
)
|
||||
assert six["similarHidden"] == "" # 화면에서 뺀 갈래만이면 줄 숨김(PLAN 77-1)
|
||||
# 띠 폭이 없어도 줄은 선다 · 받침 있는 이름은 「과」
|
||||
assert "폭" not in six["similarNoBand"] and "공사비 어림 최소와 거의" in six["similarNoBand"]
|
||||
assert "폭" not in six["similarNoBand"] and "비중 계산과 거의" in six["similarNoBand"]
|
||||
# 없으면 줄 숨김 — 빈 목록 · 칸 없음 · 선택 없음
|
||||
assert six["similarEmpty"] == "" and six["similarMissing"] == "" and six["similarNull"] == ""
|
||||
assert six["similarUnknown"] == [] # 이름 · 거리를 모르는 항목(옛 short 포함)은 버림
|
||||
|
||||
Reference in New Issue
Block a user