fix(B05): 종단곡선 기준을 L로 전환하고 테이블 곡선 R·L 누락 수정
영구저장소를 직접 확인하니 R·L은 정상 저장돼 있었고, 안 보인 원인은 둘이었다. 호가 너무 짧았다 - 배관 정착 선형은 변화점 대수차가 1~3%로 작아 R 기준(250m)에서 L = R × A 가 3~7m. 350m 연장 도면에서 화면상 10~20px이라 직선과 구분되지 않았다. - 곡선 기준을 R에서 L로 바꿨다: pipe_anchor_curve_length_m = 15.0 신설, R = L / A 로 역산(법정 최소반경 하한 유지). 표고 보정 반복마다 대수차가 변하므로 R도 매 회 다시 역산한다. 변화점마다 호 길이가 같아 도면이 고르다. 테이블 곡선 행이 규칙 측점만 순회했다 - buildCurveRows가 20m 격자 측점만 돌아 배관 변화점(84.3m 등)의 R·L 셀이 아예 그려지지 않았다. 격자 밖 변화점 셀을 추가해 선택·하이라이트와 무관하게 기본으로 값이 보이게 했다. 검증: solve 재실행 후 84.30/200.92/275.71m 호 길이 정확히 15.00m, 149.73m만 대수차 25.2%로 최소반경 바닥에 걸려 46.07m(정상). 배관 4곳 계획고=지반고 유지. typecheck·vite build·ruff·B03 테스트 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ from B05_wf2_Route.B05_wf2_Route_Engine_Grade_Solver import (
|
|||||||
solve_alignment_elevations,
|
solve_alignment_elevations,
|
||||||
station_breakpoints,
|
station_breakpoints,
|
||||||
)
|
)
|
||||||
|
from config.config_system import FOREST_ROAD_PROFILE_ALIGNMENT
|
||||||
|
|
||||||
ALIGNMENT_PROFILE_ID = "design_grade_line"
|
ALIGNMENT_PROFILE_ID = "design_grade_line"
|
||||||
ALIGNMENT_BASIS = "station_alignment"
|
ALIGNMENT_BASIS = "station_alignment"
|
||||||
@@ -133,9 +134,10 @@ def design_pipe_anchored_profile(
|
|||||||
대칭 종단곡선은 원래 변화점(꼭짓점)을 지나지 않으므로, 변화점 표고를 반복 보정해
|
대칭 종단곡선은 원래 변화점(꼭짓점)을 지나지 않으므로, 변화점 표고를 반복 보정해
|
||||||
**곡선이 정확히 배관 지반고를 통과**하도록 맞춘다(중앙종거만큼 꼭짓점을 밀어낸다).
|
**곡선이 정확히 배관 지반고를 통과**하도록 맞춘다(중앙종거만큼 꼭짓점을 밀어낸다).
|
||||||
|
|
||||||
호 반경은 설계 기준의 종단곡선 최소 반경(`options.min_vertical_radius_m`,
|
호는 **길이 L을 기준**으로 잡는다(`pipe_anchor_curve_length_m`). 변화점 사이 대수차 A가
|
||||||
config_system 법정 기준 해석값)을 기본으로 각 변화점에 지정하며, 이 값은 편집
|
작아 R을 고정하면 L = R × A 가 변화점마다 널뛰기 때문이다. R = L / A 로 역산해 저장하며
|
||||||
델타(curve_radii)로 저장돼 사용자가 B05 테이블에서 그대로 고칠 수 있다.
|
법정 최소반경(`options.min_vertical_radius_m`) 아래로는 내려가지 않게 눌러 준다.
|
||||||
|
이 R은 편집 델타(curve_radii)로 저장돼 사용자가 B05 테이블에서 그대로 고칠 수 있다.
|
||||||
기울기 위반은 막지 않고 경고로 남긴다 — 배관 위치가 우선이고 조정은 사용자 몫이다.
|
기울기 위반은 막지 않고 경고로 남긴다 — 배관 위치가 우선이고 조정은 사용자 몫이다.
|
||||||
"""
|
"""
|
||||||
options.validate()
|
options.validate()
|
||||||
@@ -185,18 +187,30 @@ def design_pipe_anchored_profile(
|
|||||||
target[0] = fixed[0]
|
target[0] = fixed[0]
|
||||||
target[-1] = fixed[1]
|
target[-1] = fixed[1]
|
||||||
|
|
||||||
# 각 배관 변화점의 호 반경 기본값 — 설계 기준의 종단곡선 최소 반경.
|
target_length = float(FOREST_ROAD_PROFILE_ALIGNMENT["pipe_anchor_curve_length_m"])
|
||||||
anchor_radii = {chainage_key(a): float(options.min_vertical_radius_m) for a in anchors}
|
min_radius = float(options.min_vertical_radius_m)
|
||||||
edits = dict(edits or {})
|
edits = dict(edits or {})
|
||||||
edits["curve_radii"] = {**anchor_radii, **(edits.get("curve_radii") or {})}
|
user_radii = dict(edits.get("curve_radii") or {})
|
||||||
|
|
||||||
|
def radii_for(nodes_z: np.ndarray) -> dict[str, float]:
|
||||||
|
"""현 표고에서의 대수차로 R = L / A 를 역산한다(사용자 지정 R이 있으면 그쪽이 이긴다)."""
|
||||||
|
spans = base_s[1:] - base_s[:-1]
|
||||||
|
grades = (nodes_z[1:] - nodes_z[:-1]) / np.where(spans > 0, spans, 1.0)
|
||||||
|
resolved: dict[str, float] = {}
|
||||||
|
for index in range(1, len(base_s) - 1):
|
||||||
|
delta = abs(float(grades[index] - grades[index - 1]))
|
||||||
|
radius = target_length / delta if delta > 1e-9 else min_radius
|
||||||
|
resolved[chainage_key(base_s[index])] = max(radius, min_radius)
|
||||||
|
return {**resolved, **user_radii}
|
||||||
|
|
||||||
# 대칭 종단곡선은 꼭짓점(변화점)을 지나지 않는다 — 곡선이 배관 지반고를 통과하도록
|
# 대칭 종단곡선은 꼭짓점(변화점)을 지나지 않는다 — 곡선이 배관 지반고를 통과하도록
|
||||||
# 변화점 표고를 반복 보정한다(호가 안쪽으로 파고드는 중앙종거만큼 꼭짓점을 밀어낸다).
|
# 변화점 표고를 반복 보정한다(호가 안쪽으로 파고드는 중앙종거만큼 꼭짓점을 밀어낸다).
|
||||||
# 오차는 반복마다 중앙종거의 고차항만 남아 수 회면 mm 아래로 떨어진다.
|
# 표고가 움직이면 대수차도 변하므로 R도 매 회 다시 역산한다.
|
||||||
base_z = target.copy()
|
base_z = target.copy()
|
||||||
radii_for_iter = edits["curve_radii"]
|
radii = radii_for(base_z)
|
||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
curves, _curve_warnings = build_curves(base_s, base_z, policy, radii_for_iter)
|
radii = radii_for(base_z)
|
||||||
|
curves, _curve_warnings = build_curves(base_s, base_z, policy, radii)
|
||||||
plan_at = evaluate(base_s, base_z, curves, base_s)
|
plan_at = evaluate(base_s, base_z, curves, base_s)
|
||||||
error = target - plan_at
|
error = target - plan_at
|
||||||
error[0] = 0.0
|
error[0] = 0.0
|
||||||
@@ -204,6 +218,7 @@ def design_pipe_anchored_profile(
|
|||||||
if float(np.max(np.abs(error))) < 1e-4:
|
if float(np.max(np.abs(error))) < 1e-4:
|
||||||
break
|
break
|
||||||
base_z = base_z + error
|
base_z = base_z + error
|
||||||
|
edits["curve_radii"] = radii
|
||||||
|
|
||||||
alignment = build_alignment(
|
alignment = build_alignment(
|
||||||
base_s=base_s,
|
base_s=base_s,
|
||||||
|
|||||||
@@ -292,7 +292,12 @@ function curveTitle(curve: AlignmentCurve): string {
|
|||||||
* 곡선이 없는 측점에도 **빈 칸을 만든다**. 변화점에만 셀을 두면 세로 구분선이 띄엄띄엄
|
* 곡선이 없는 측점에도 **빈 칸을 만든다**. 변화점에만 셀을 두면 세로 구분선이 띄엄띄엄
|
||||||
* 끊겨 위쪽 측점값 행들과 격자가 맞지 않는다.
|
* 끊겨 위쪽 측점값 행들과 격자가 맞지 않는다.
|
||||||
*/
|
*/
|
||||||
function buildCurveRows(options: ProfileTableOptions, centers: number[]): HTMLElement[] {
|
function buildCurveRows(
|
||||||
|
options: ProfileTableOptions,
|
||||||
|
centers: number[],
|
||||||
|
x: (chainage: number) => number,
|
||||||
|
cellWidth: number,
|
||||||
|
): HTMLElement[] {
|
||||||
const { alignment, onCurveRadiusChange } = options;
|
const { alignment, onCurveRadiusChange } = options;
|
||||||
const lengthRow = createRow(
|
const lengthRow = createRow(
|
||||||
"b05-profile-table__row--curve is-group-start",
|
"b05-profile-table__row--curve is-group-start",
|
||||||
@@ -339,6 +344,42 @@ function buildCurveRows(options: ProfileTableOptions, centers: number[]): HTMLEl
|
|||||||
placeCell(radiusRow, centers[index], radiusCell);
|
placeCell(radiusRow, centers[index], radiusCell);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 규칙 측점 격자에 없는 변화점(배관 자리처럼 임의 chainage에 승격된 점)의 곡선도
|
||||||
|
// **기본으로** 값을 보여 준다 — 격자에만 셀을 두면 배관 변화점의 R·L이 표에서 통째로
|
||||||
|
// 빠져 "R이 안 들어간 것"처럼 읽힌다(2026-08-03 사용자 보고).
|
||||||
|
const stationKeys = new Set(alignment.stations.map((row) => row.chainage_m.toFixed(3)));
|
||||||
|
alignment.curves.forEach((curve) => {
|
||||||
|
const key = curve.chainage_m.toFixed(3);
|
||||||
|
if (stationKeys.has(key)) return;
|
||||||
|
const lengthCell = element("span", "b05-profile-table__curve is-floating");
|
||||||
|
const radiusCell = element("span", "b05-profile-table__curve is-floating");
|
||||||
|
radiusCell.append(
|
||||||
|
curveInput(
|
||||||
|
curve,
|
||||||
|
curve.r_m.toFixed(1),
|
||||||
|
`종단곡선 반경 R (m)
|
||||||
|
${curveTitle(curve)}`,
|
||||||
|
(r) => r,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
lengthCell.append(
|
||||||
|
curveInput(
|
||||||
|
curve,
|
||||||
|
curve.l_m.toFixed(2),
|
||||||
|
`종단곡선 길이 L (m)
|
||||||
|
${curveTitle(curve)}`,
|
||||||
|
(l) => (curve.l_m > 1e-9 ? (l * curve.r_m) / curve.l_m : null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
[lengthCell, radiusCell].forEach((cell) => {
|
||||||
|
if (curve.skip_allowed) cell.classList.add("is-optional");
|
||||||
|
if (curve.omitted) cell.classList.add("is-omitted");
|
||||||
|
cell.style.width = `${cellWidth}px`;
|
||||||
|
});
|
||||||
|
placeCell(lengthRow, x(curve.chainage_m), lengthCell);
|
||||||
|
placeCell(radiusRow, x(curve.chainage_m), radiusCell);
|
||||||
|
});
|
||||||
|
|
||||||
function curveInput(
|
function curveInput(
|
||||||
curve: AlignmentCurve,
|
curve: AlignmentCurve,
|
||||||
value: string,
|
value: string,
|
||||||
@@ -540,7 +581,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
|
|||||||
});
|
});
|
||||||
table.append(row);
|
table.append(row);
|
||||||
});
|
});
|
||||||
table.append(...buildCurveRows(options, centers));
|
table.append(...buildCurveRows(options, centers, x, cellWidth));
|
||||||
|
|
||||||
// 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다.
|
// 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다.
|
||||||
// 규칙 측점은 종점 이동을 반영한 `centers[index]`, 비정규 측점은 `x(chainage)`를 중심으로 쓴다.
|
// 규칙 측점은 종점 이동을 반영한 `centers[index]`, 비정규 측점은 `x(chainage)`를 중심으로 쓴다.
|
||||||
|
|||||||
@@ -1146,3 +1146,10 @@
|
|||||||
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
|
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
|
||||||
outline: 1px solid var(--color-primary);
|
outline: 1px solid var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 규칙 측점 격자 밖 변화점(배관 자리)의 곡선 R·L 셀 — 격자 셀 위에 얹혀 값을 보인다. */
|
||||||
|
.b05-profile-table__curve.is-floating {
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--color-surface-raised);
|
||||||
|
outline: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|||||||
@@ -554,6 +554,14 @@ FOREST_ROAD_PROFILE_ALIGNMENT = {
|
|||||||
# 도면상 곡선이 거의 드러나지 않는다. 곡선을 뚜렷하게 보려면 이 비율을 크게 올린다
|
# 도면상 곡선이 거의 드러나지 않는다. 곡선을 뚜렷하게 보려면 이 비율을 크게 올린다
|
||||||
# (예: 20.0 → R=400m, A=10%일 때 L=40m).
|
# (예: 20.0 → R=400m, A=10%일 때 L=40m).
|
||||||
"curve_radius_ratio": 0.40,
|
"curve_radius_ratio": 0.40,
|
||||||
|
# **배관 정착 계획선(1차 로직)의 기본 종단곡선 길이 L(m).**
|
||||||
|
# 이 선형은 변화점 사이 대수차 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,
|
||||||
# 인접 직선 길이 대비 곡선 반쪽이 점유할 수 있는 최대 비율.
|
# 인접 직선 길이 대비 곡선 반쪽이 점유할 수 있는 최대 비율.
|
||||||
# 0.45면 짧은 쪽 직선의 55%가 항상 직선으로 남아 좌우 곡선이 겹치지 않는다.
|
# 0.45면 짧은 쪽 직선의 55%가 항상 직선으로 남아 좌우 곡선이 겹치지 않는다.
|
||||||
"curve_tangent_max_ratio": 0.45,
|
"curve_tangent_max_ratio": 0.45,
|
||||||
|
|||||||
Reference in New Issue
Block a user