fix(B05): 최소고 가드를 편집 단일 창구로 이관 + 되돌리기가 구조물 위치까지 복원
2026-09-02 화면 검증에서 나온 미해결 2건 처리. - 최소고 가드: `blocksMinCover` 가 `_Profile_Render` 측점 끌기 한 곳에만 있어 [직선화]·[쉬프트]·틸팅·방향키가 그냥 지나감. 판정 로직을 `_Profile_MinCover.blocksMinCover()` 로 옮기고 모든 편집이 지나는 `_Profile_Panel.applyEdits` 한 곳에서만 호출. 화면 모듈의 사본·죽은 배선 (`enforceMinCover` ctx 필드, 미사용 import)은 제거. - 되돌리기 범위: 이력이 `b05:`·`b05-`·`b06:`·`b06-` 세션 키만 스냅샷하는데 구조물(관) 위치는 세션 밖이라 undo 가 계획선만 되돌림. `_Profile_Panel_Tools` 가 기록 직전에 관 목록을 `b05:pipes` 로 흘리고 복원 뒤 되읽어 반영. 사용자 확정 ⑥ 「범위는 B05 페이지 조작 전부」 충족. 검증(공용 브라우저, 용화 검증 프로젝트 route_id 126): - 최소고 강제 켠 상태로 관 측점 264.060 방향키 ↓ → 편집 미적용 + 경고 "배수관 Ø800 — 최소 계획고(지반 +1.3m) 아래로 내려갈 수 없습니다." - 구조물 측점 264.060 → 263.960 이동 후 ↶ 1회 → 264.060 복귀, `b05:pipes` 목록도 264.06 으로 복귀. - `tsc --noEmit` 0, prettier 통과, pytest 366 passed / 17 skipped / 0 failed. - 파일 줄수: Panel 699 · Panel_Tools 197 · Render 350 · MinCover 188 (700줄 제한 준수). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
* 거친다. 그래프·테이블·유토곡선이 같은 X 매핑을 쓰는 규칙은 그대로다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createLongitudinalProfile,
|
||||
longitudinalMinimumWidth,
|
||||
@@ -18,15 +17,13 @@ import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch
|
||||
import { normalizedLongitudinal, toDesignProfile } from "./B05_Profile_UI_Profile_Data";
|
||||
import {
|
||||
adjustStation,
|
||||
buildAlignment,
|
||||
controlElevationAt,
|
||||
setCurveRadius,
|
||||
type AlignmentBase,
|
||||
type AlignmentEdits,
|
||||
type ProfileAlignment,
|
||||
} from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { createEditOverlay } from "./B05_Profile_UI_Profile_Edit";
|
||||
import { findMinCoverViolations, type MinCoverPoint } from "./B05_Profile_UI_Profile_MinCover";
|
||||
import type { MinCoverPoint } from "./B05_Profile_UI_Profile_MinCover";
|
||||
import { buildStickyYAxis, type RouteMassHaulDrawParams } from "./B05_Profile_UI_Profile_MassHaul";
|
||||
import { createProfileTable } from "./B05_Profile_UI_Profile_Table";
|
||||
import {
|
||||
@@ -64,10 +61,9 @@ export interface ProfileRenderContext {
|
||||
base: () => AlignmentBase | null;
|
||||
stationInterval: () => number | undefined;
|
||||
irregularStations: () => IrregularStation[];
|
||||
/** 횡단배수 최소 계획고 대상(시설·제원 반영) — 편집 차단 가드가 쓴다. */
|
||||
/** 횡단배수 최소 계획고 대상(시설·제원 반영) — 요약줄 경고 표시에 쓴다.
|
||||
* 편집 차단 가드는 `_Profile_Panel.applyEdits` 한 곳으로 옮겼다(2026-09-02). */
|
||||
minCoverTargets: () => MinCoverPoint[];
|
||||
/** 최소고를 편집에서 강제할지 — 꺼져 있으면 차단하지 않는다(2026-09-01, 기본 해제). */
|
||||
enforceMinCover: () => boolean;
|
||||
structures: () => StructureInstance[];
|
||||
structureTypes: () => StructureType[];
|
||||
selectedStationId: () => string | null;
|
||||
@@ -291,55 +287,6 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
// 아래로 밀려 스크롤 시 축이 화면에 안 보인다(2026-08-04 확인, B06과 같은 규칙).
|
||||
if (yAxis) chartWrap.prepend(buildStickyYAxis(yAxis, chartHeight));
|
||||
if (alignment) {
|
||||
// 횡단배수 최소 계획고 가드(2026-08-23 개편): 편집 **후보**로 정렬을 미리 계산해
|
||||
// 시설별 최소고(배수관 관경+토피 · BOX암거 구체높이+토피 · 세월교 +물넘이 몫,
|
||||
// 산식은 minCoverPoints 동일 원천) 위반이 **새로 생기거나 커지면** 차단한다.
|
||||
// 측점 ▼뿐 아니라 구간 ⇧⇩, 이웃 틸트가 종단곡선(중앙종거)을 거쳐 배관 계획고를
|
||||
// 내리는 경로까지 같은 가드로 잡는다 — 기존 관경 고정 산식은 구간 쉬프트를 아예
|
||||
// 안 막았고 BOX암거·세월교를 과소 차단했다(2026-08-23 사용자 보고).
|
||||
// 이미 위반이면 악화만 막는다 — 복구 편집(올림)은 항상 허용돼야 한다.
|
||||
const groundAt = (chainageM: number): number | null => {
|
||||
if (!base || !base.chainage.length) return null;
|
||||
const { chainage: xs, ground: ys } = base;
|
||||
if (chainageM <= xs[0]) return ys[0];
|
||||
if (chainageM >= xs[xs.length - 1]) return ys[ys.length - 1];
|
||||
for (let i = 1; i < xs.length; i += 1) {
|
||||
if (chainageM > xs[i]) continue;
|
||||
const span = xs[i] - xs[i - 1];
|
||||
if (span <= 0) return ys[i];
|
||||
return ys[i - 1] + (ys[i] - ys[i - 1]) * ((chainageM - xs[i - 1]) / span);
|
||||
}
|
||||
return ys[ys.length - 1];
|
||||
};
|
||||
const blocksMinCover = (next: AlignmentEdits): boolean => {
|
||||
if (!base) return false;
|
||||
// 최소고 강제가 꺼져 있으면 막지 않는다(2026-09-01 사용자 지시 — 기본 해제).
|
||||
// 부족분은 상단 표시줄 경고(minCoverWarningText)로 계속 알린다.
|
||||
if (!ctx.enforceMinCover()) return false;
|
||||
const targets = ctx.minCoverTargets();
|
||||
if (!targets.length) return false;
|
||||
// 판정점 = 제어점 z(라운드 중심). 곡선 샘플로 재면 이웃 틸팅이 라운드 형상만
|
||||
// 바꿔도 잠긴다(2026-08-23 사용자: "옆 지점 틸팅에 락 — 말이 안 됨").
|
||||
const candidate = buildAlignment(base, next);
|
||||
const planned = findMinCoverViolations(targets, groundAt, (chainageM) =>
|
||||
controlElevationAt(candidate, chainageM),
|
||||
);
|
||||
if (!planned.length) return false;
|
||||
const current = new Map(
|
||||
findMinCoverViolations(targets, groundAt, (chainageM) =>
|
||||
controlElevationAt(alignment, chainageM),
|
||||
).map((violation) => [violation.chainage_m, violation.shortfall_m]),
|
||||
);
|
||||
const worsened = planned.find(
|
||||
(violation) => violation.shortfall_m > (current.get(violation.chainage_m) ?? 0) + 1e-6,
|
||||
);
|
||||
if (!worsened) return false;
|
||||
showToast(
|
||||
`${worsened.label} — 최소 계획고(지반 +${worsened.clearance_m.toFixed(1)}m) 아래로 내려갈 수 없습니다.`,
|
||||
"warning",
|
||||
);
|
||||
return true;
|
||||
};
|
||||
chartWrap.append(
|
||||
createEditOverlay({
|
||||
alignment,
|
||||
@@ -351,11 +298,11 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
(entry) =>
|
||||
entry.chainage_m >= 0 && entry.chainage_m <= maxChainageOf(longitudinal) + 1e-6,
|
||||
),
|
||||
// 최소고 가드는 `_Profile_Panel.applyEdits` 한 곳에 있다 — 여기 따로 걸면
|
||||
// 다른 편집 경로(직선화·쉬프트·틸팅·방향키)와 규칙이 갈린다(2026-09-02).
|
||||
onStation: (chainage, delta) => {
|
||||
if (!base) return;
|
||||
const next = adjustStation(base, store.edits(), chainage, delta);
|
||||
if (blocksMinCover(next)) return;
|
||||
ctx.applyEdits(next);
|
||||
ctx.applyEdits(adjustStation(base, store.edits(), chainage, delta));
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user