|
|
|
@@ -14,11 +14,11 @@ import {
|
|
|
|
|
longitudinalMinimumWidth,
|
|
|
|
|
} from "../B06_Section/B06_Section_UI_Longitudinal";
|
|
|
|
|
import { LONG_PAD } from "../B06_Section/B06_Section_UI_Section_Common";
|
|
|
|
|
import { MIN_PIPE_COVER_M } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
|
|
|
|
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
|
|
|
|
import { normalizedLongitudinal, toDesignProfile } from "./B05_Profile_UI_Profile_Data";
|
|
|
|
|
import {
|
|
|
|
|
adjustStation,
|
|
|
|
|
buildAlignment,
|
|
|
|
|
planElevationAt,
|
|
|
|
|
setCurveRadius,
|
|
|
|
|
shiftSegment,
|
|
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
|
|
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 { buildStickyYAxis, type RouteMassHaulDrawParams } from "./B05_Profile_UI_Profile_MassHaul";
|
|
|
|
|
import { createProfileTable } from "./B05_Profile_UI_Profile_Table";
|
|
|
|
|
import {
|
|
|
|
@@ -39,11 +40,7 @@ import {
|
|
|
|
|
STATION_SPACING_PX,
|
|
|
|
|
TABLE_ROW_COUNT,
|
|
|
|
|
} from "./B05_Profile_UI_Profile_Layout";
|
|
|
|
|
import {
|
|
|
|
|
irregularStationId,
|
|
|
|
|
isPipeStation,
|
|
|
|
|
type IrregularStation,
|
|
|
|
|
} from "./B05_Profile_UI_IrregularStations";
|
|
|
|
|
import { irregularStationId, type IrregularStation } from "./B05_Profile_UI_IrregularStations";
|
|
|
|
|
import { mountStructureMenu } from "./B05_Profile_UI_Profile_Structures";
|
|
|
|
|
import { buildStructureLane, STRUCTURE_LANE_HEIGHT_PX } from "./B05_Profile_UI_Structures_Marks";
|
|
|
|
|
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
|
|
|
@@ -68,6 +65,8 @@ export interface ProfileRenderContext {
|
|
|
|
|
base: () => AlignmentBase | null;
|
|
|
|
|
stationInterval: () => number | undefined;
|
|
|
|
|
irregularStations: () => IrregularStation[];
|
|
|
|
|
/** 횡단배수 최소 계획고 대상(시설·제원 반영) — 편집 차단 가드가 쓴다. */
|
|
|
|
|
minCoverTargets: () => MinCoverPoint[];
|
|
|
|
|
structures: () => StructureInstance[];
|
|
|
|
|
structureTypes: () => StructureType[];
|
|
|
|
|
selectedStationId: () => string | null;
|
|
|
|
@@ -270,9 +269,13 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
|
|
|
|
// 아래로 밀려 스크롤 시 축이 화면에 안 보인다(2026-08-04 확인, B06과 같은 규칙).
|
|
|
|
|
if (yAxis) chartWrap.prepend(buildStickyYAxis(yAxis, chartHeight));
|
|
|
|
|
if (alignment) {
|
|
|
|
|
// 배수관 측점 계획고 하향 차단(2026-08-19): 관은 지반선상 매설이라(교본 8장)
|
|
|
|
|
// 계획고가 지반고 + 관경 + 최소 토피 아래로 내려가면 관이 노면을 뚫는다.
|
|
|
|
|
// 내림(▼)만 막고 올림(▲)·다른 측점은 그대로 둔다 — 판단은 사용자 몫, 한계만 지킨다.
|
|
|
|
|
// 횡단배수 최소 계획고 가드(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;
|
|
|
|
@@ -286,19 +289,26 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
|
|
|
|
}
|
|
|
|
|
return ys[ys.length - 1];
|
|
|
|
|
};
|
|
|
|
|
const blocksPipeLowering = (chainageM: number, delta: number): boolean => {
|
|
|
|
|
if (delta >= 0) return false;
|
|
|
|
|
const pipe = irregularStations.find(
|
|
|
|
|
(entry) => isPipeStation(entry) && Math.abs(entry.chainage_m - chainageM) < 0.02,
|
|
|
|
|
const blocksMinCover = (next: AlignmentEdits): boolean => {
|
|
|
|
|
if (!base) return false;
|
|
|
|
|
const targets = ctx.minCoverTargets();
|
|
|
|
|
if (!targets.length) return false;
|
|
|
|
|
const candidate = buildAlignment(base, next);
|
|
|
|
|
const planned = findMinCoverViolations(targets, groundAt, (chainageM) =>
|
|
|
|
|
planElevationAt(candidate, chainageM),
|
|
|
|
|
);
|
|
|
|
|
if (!pipe) return false;
|
|
|
|
|
const ground = groundAt(chainageM);
|
|
|
|
|
if (ground === null) return false;
|
|
|
|
|
const diameterM = (pipe.diameterMm ?? 1000) / 1000;
|
|
|
|
|
const minPlan = ground + diameterM + MIN_PIPE_COVER_M;
|
|
|
|
|
if (planElevationAt(alignment, chainageM) + delta >= minPlan - 1e-6) return false;
|
|
|
|
|
if (!planned.length) return false;
|
|
|
|
|
const current = new Map(
|
|
|
|
|
findMinCoverViolations(targets, groundAt, (chainageM) =>
|
|
|
|
|
planElevationAt(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(
|
|
|
|
|
`배수관 측점 — 관 매설 최소 높이(지반 + 관경 ${diameterM.toFixed(1)}m + 토피 ${MIN_PIPE_COVER_M.toFixed(1)}m)에 도달해 더 내릴 수 없습니다.`,
|
|
|
|
|
`${worsened.label} — 최소 계획고(지반 +${worsened.clearance_m.toFixed(1)}m) 아래로 내려갈 수 없습니다.`,
|
|
|
|
|
"warning",
|
|
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
@@ -315,11 +325,17 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
|
|
|
|
entry.chainage_m >= 0 && entry.chainage_m <= maxChainageOf(longitudinal) + 1e-6,
|
|
|
|
|
),
|
|
|
|
|
onStation: (chainage, delta) => {
|
|
|
|
|
if (!base || blocksPipeLowering(chainage, delta)) return;
|
|
|
|
|
ctx.applyEdits(adjustStation(base, store.edits(), chainage, delta));
|
|
|
|
|
if (!base) return;
|
|
|
|
|
const next = adjustStation(base, store.edits(), chainage, delta);
|
|
|
|
|
if (blocksMinCover(next)) return;
|
|
|
|
|
ctx.applyEdits(next);
|
|
|
|
|
},
|
|
|
|
|
onSegment: (segment, delta) => {
|
|
|
|
|
if (!base) return;
|
|
|
|
|
const next = shiftSegment(base, store.edits(), segment, delta);
|
|
|
|
|
if (blocksMinCover(next)) return;
|
|
|
|
|
ctx.applyEdits(next);
|
|
|
|
|
},
|
|
|
|
|
onSegment: (segment, delta) =>
|
|
|
|
|
base && ctx.applyEdits(shiftSegment(base, store.edits(), segment, delta)),
|
|
|
|
|
onResetStation: (chainage) => store.resetStation(chainage),
|
|
|
|
|
// 구간 원복 = 양 끝 측점 오프셋 삭제(측점 원복 연산 ×2).
|
|
|
|
|
onResetSegment: (segment) => {
|
|
|
|
|