feat(B06): 다단 기슭막이 구간값을 단별로 손수 잡는다
다단은 소유 벽 연장에 종속이라 길이·전/후를 따로 못 잡았다. 계곡부·능선부에서는 아래 단일수록 연장이 달라져 자동 규칙으로 못 잡으므로 수동 입력으로 간다. 조정창 UI는 기준벽 것을 그대로 쓴다(길이·기준측점 전·후 세 행). - Const: EXTRA_SPAN_DEFAULT(10m·5/5) + applySpanPatch()로 길이↔전/후 산식을 기준벽과 공용화 - Station_Controls: StructureSpanControl에 tierValuesFor/updateTier 추가. 값은 세션(b06:extraspan)에 담고 캐시(design.extra_spans)에도 얹는다. 단 수를 줄이면 그 단의 구간값도 지운다 - View_Structure: 다단 키면 단별 제어기로, 기준벽은 종전 경로 - Wire: culvertReach에 단별 연장을 더하고, 링크 카드에서 각 단을 자기 구간값으로 판정한다(tierSpanOf) - Corridor_Structures: 3D 스윕 범위를 단별 구간값으로 - 저장: CrossSectionPatch.extra_spans(프론트 payload + WallSpanPatch 스키마 + 확정 병합) — [저장]·[확정] 때 정본에 남는다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import type { CrossSection, CulvertSideSpec } from "../B06_Section/B06_Section_A
|
||||
import type { WallAdjust } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
|
||||
import { ZERO_ADJUST } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
|
||||
import { computeCulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
||||
import { restrictToSide } from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { restrictToSide, tierSpanOf } from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { pipeWallThicknessM } from "../B06_Section/B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { CulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
||||
import {
|
||||
@@ -538,13 +538,16 @@ export function buildCorridorStructures(
|
||||
if (hiddenPipe) pushSwept("revet", wall.points, span.beforeM, span.afterM);
|
||||
else pushPierced("revet", wall.points, span.beforeM, span.afterM, culvertBore);
|
||||
}
|
||||
// 다단(성토부) 벽은 유출측 연장을, 집수정 계류측 다단은 집수정 연장을 따른다.
|
||||
for (const wall of layout.extraWalls) {
|
||||
pushSwept("revet", wall.points, outletSpan.beforeM, outletSpan.afterM);
|
||||
}
|
||||
for (const wall of layout.basinExtras) {
|
||||
pushSwept("revet", wall.points, basinSpan.beforeM, basinSpan.afterM);
|
||||
}
|
||||
// 다단은 **단별 구간값**을 따른다(2026-08-29 사용자 — 단마다 연장이 다르다).
|
||||
// 값이 없는 단은 고정 기본 10m(5/5)로 선다(`tierSpanOf`).
|
||||
layout.extraWalls.forEach((wall, i) => {
|
||||
const span = tierSpanOf(section, `extra${i}`);
|
||||
pushSwept("revet", wall.points, span.beforeM, span.afterM);
|
||||
});
|
||||
layout.basinExtras.forEach((wall, i) => {
|
||||
const span = tierSpanOf(section, `bextra${i}`);
|
||||
pushSwept("revet", wall.points, span.beforeM, span.afterM);
|
||||
});
|
||||
|
||||
if (layout.basin) {
|
||||
// 집수정 = 부재 외곽을 감싼 **직육면체**(2026-08-23 사용자).
|
||||
|
||||
@@ -323,6 +323,13 @@ export interface StoredWallAdjust {
|
||||
m: string | null;
|
||||
}
|
||||
|
||||
/** 다단 기슭막이 한 단의 종방향 구간값(길이·기준측점 전/후 m — 2026-08-29). */
|
||||
export interface StoredWallSpan {
|
||||
length_m: number;
|
||||
before_m: number;
|
||||
after_m: number;
|
||||
}
|
||||
|
||||
/** 다단 기슭막이 단 수(유출 성토부 / 집수정 계류측). */
|
||||
export interface StoredExtraWallCounts {
|
||||
outlet: number;
|
||||
@@ -372,6 +379,9 @@ export interface CrossDesign {
|
||||
revet_adjust?: Record<string, StoredWallAdjust>;
|
||||
/** 다단 기슭막이 단 수 — 유출 성토부·집수정 계류측. */
|
||||
extra_wall_counts?: StoredExtraWallCounts;
|
||||
/** 다단 기슭막이 **단별** 구간값 — 키는 벽 키("extra0"…/"bextra0"…). 기준벽 연장에
|
||||
* 종속되지 않고 단마다 따로 잡는다(2026-08-29 사용자). 없으면 기본 10m(5/5). */
|
||||
extra_spans?: Record<string, StoredWallSpan>;
|
||||
/** 연동 해제(측점별 — 2026-08-24 사용자). 옆 측점에서 연장돼 온 기슭막이의 위치
|
||||
* 4축을 이 측점에서 따로 잡는다. 구조물 추가가 아니라 3D 위치의 개별 지정이다. */
|
||||
revet_link_detached?: boolean;
|
||||
@@ -478,6 +488,7 @@ export interface CrossSectionPatch {
|
||||
ford_adjust?: StoredFordAdjust;
|
||||
box_adjust?: StoredBoxAdjust;
|
||||
extra_wall_counts?: StoredExtraWallCounts;
|
||||
extra_spans?: Record<string, StoredWallSpan>;
|
||||
/** 연동 해제(측점별)·종단경사 반영(전체 공통) — 2026-08-24 사용자. */
|
||||
revet_link_detached?: boolean;
|
||||
revet_follow_grade?: boolean;
|
||||
|
||||
@@ -132,6 +132,10 @@ async def _apply_section_edits(
|
||||
patch["box_adjust"] = patch_item.box_adjust.model_dump()
|
||||
if patch_item.extra_wall_counts is not None:
|
||||
patch["extra_wall_counts"] = patch_item.extra_wall_counts.model_dump()
|
||||
if patch_item.extra_spans is not None:
|
||||
patch["extra_spans"] = {
|
||||
wall: span.model_dump() for wall, span in patch_item.extra_spans.items()
|
||||
}
|
||||
if patch_item.revet_link_detached is not None:
|
||||
patch["revet_link_detached"] = patch_item.revet_link_detached
|
||||
if patch_item.revet_follow_grade is not None:
|
||||
|
||||
@@ -84,6 +84,14 @@ class WallAdjustPatch(BaseModel):
|
||||
m: str | None = None
|
||||
|
||||
|
||||
class WallSpanPatch(BaseModel):
|
||||
"""다단 기슭막이 한 단의 종방향 구간값(길이·기준측점 전/후 m — 2026-08-29)."""
|
||||
|
||||
length_m: float = Field(default=10.0, ge=0.0, le=200.0)
|
||||
before_m: float = Field(default=5.0, ge=0.0, le=100.0)
|
||||
after_m: float = Field(default=5.0, ge=0.0, le=100.0)
|
||||
|
||||
|
||||
class ExtraWallCountsPatch(BaseModel):
|
||||
"""다단 기슭막이 단 수 — 유출 성토부(outlet)·집수정 계류측(basin)."""
|
||||
|
||||
@@ -138,6 +146,9 @@ class CrossSectionPatch(BaseModel):
|
||||
# BOX암거 구체 조작값(좌·우 끝 길이·표고) — 2026-08-25 사용자.
|
||||
box_adjust: BoxAdjustPatch | None = None
|
||||
extra_wall_counts: ExtraWallCountsPatch | None = None
|
||||
# 다단 기슭막이 단별 구간값 — 키는 벽 키("extra0"…/"bextra0"…). 기준벽 연장에
|
||||
# 종속되지 않고 사용자가 단마다 넣는다(2026-08-29 사용자 확정).
|
||||
extra_spans: dict[str, WallSpanPatch] | None = None
|
||||
# 연동 기슭막이 옵션(2026-08-24 사용자). 연동 해제는 측점별, 종단경사 반영은
|
||||
# 기슭막이 한 벌 전체 공통이라 소유 측점에만 실린다.
|
||||
revet_link_detached: bool | None = None
|
||||
|
||||
@@ -311,6 +311,33 @@ export const SPAN_OPTION_KEYS: Record<SpanRole, { length: string; before: string
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 다단(추가) 기슭막이 한 단의 **기본 구간값** — 고정 10m(전 5·후 5, 2026-08-29 사용자
|
||||
* 확정). 기준벽 연장을 상속하지 않는다: 계곡부·능선부에서 아래 단일수록 연장이 달라져
|
||||
* 자동 규칙으로 못 잡고 사용자가 손으로 넣는다.
|
||||
*/
|
||||
export const EXTRA_SPAN_DEFAULT: SpanValues = { lengthM: 10, beforeM: 5, afterM: 5 };
|
||||
|
||||
/**
|
||||
* 구간값 한 벌에 조작을 얹는다 — 기준벽·다단이 같은 산식을 쓴다.
|
||||
* 길이를 바꾸면 **늘어난 몫만** 지금 비율대로 나눠 담는다(매번 총길이에서 비율로 다시
|
||||
* 계산하면 0.1m 반올림이 쌓여 5.0/5.0이 5.6/5.4로 어긋난다 — 2026-08-24 화면 실측).
|
||||
* 전·후를 바꾸면 길이는 둘의 합이다.
|
||||
*/
|
||||
export function applySpanPatch(current: SpanValues, patch: Partial<SpanValues>): SpanValues {
|
||||
let { beforeM, afterM } = current;
|
||||
if (patch.lengthM !== undefined) {
|
||||
const total = Math.max(patch.lengthM, 0);
|
||||
const delta = total - current.lengthM;
|
||||
const ratio = current.lengthM > 1e-9 ? current.beforeM / current.lengthM : 0.5;
|
||||
beforeM = Math.max(Math.round((current.beforeM + delta * ratio) * 10) / 10, 0);
|
||||
afterM = Math.max(Math.round((total - beforeM) * 10) / 10, 0);
|
||||
}
|
||||
if (patch.beforeM !== undefined) beforeM = Math.max(patch.beforeM, 0);
|
||||
if (patch.afterM !== undefined) afterM = Math.max(patch.afterM, 0);
|
||||
return { lengthM: Math.round((beforeM + afterM) * 10) / 10, beforeM, afterM };
|
||||
}
|
||||
|
||||
/** 소유 측점의 구간값(길이·전/후) — 저장 옵션과 기본값을 함께 푼 결과. */
|
||||
export const spanValuesOf = (owner: CrossSection, role: SpanRole): SpanValues | null => {
|
||||
const spec = role === "outlet" ? owner.culvert?.outlet : owner.culvert?.inlet;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
materialLimit,
|
||||
revetSpanOfSpec,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import { EXTRA_SPAN_DEFAULT } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { StructureSpan } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
@@ -57,6 +58,10 @@ export interface StructureSpanControl {
|
||||
ownerOf: (section: CrossSection) => CrossSection | null;
|
||||
valuesFor: (section: CrossSection, role: SpanRole) => SpanValues | null;
|
||||
update: (section: CrossSection, role: SpanRole, patch: Partial<SpanValues>) => void;
|
||||
/** 다단 한 단의 구간값 — 기준벽과 달리 **단마다 따로** 잡는다(2026-08-29 사용자).
|
||||
* 값은 세션에 담기고 [저장]·[확정] 때 정본으로 간다. 없으면 기본 10m(5/5). */
|
||||
tierValuesFor: (section: CrossSection, key: RevetKey) => SpanValues;
|
||||
updateTier: (section: CrossSection, key: RevetKey, patch: Partial<SpanValues>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +137,17 @@ export interface CulvertLink {
|
||||
deltaM: number;
|
||||
}
|
||||
|
||||
/** 구조물이 종방향으로 덮는 범위(기준측점 전/후 m) — 기슭막이 연장·집수정 연장 중 큰 값. */
|
||||
/**
|
||||
* 다단 한 단의 구간값 — 단별로 따로 잡은 값(`design.extra_spans`)이 있으면 그것,
|
||||
* 없으면 고정 기본 10m(5/5). 기준벽 연장을 상속하지 않는다(2026-08-29 사용자).
|
||||
*/
|
||||
export function tierSpanOf(section: CrossSection, key: string): StructureSpan {
|
||||
const stored = section.design?.extra_spans?.[key];
|
||||
if (!stored) return { ...EXTRA_SPAN_DEFAULT };
|
||||
return { beforeM: Math.max(stored.before_m, 0), afterM: Math.max(stored.after_m, 0) };
|
||||
}
|
||||
|
||||
/** 구조물이 종방향으로 덮는 범위(기준측점 전/후 m) — 기슭막이·집수정·다단 중 큰 값. */
|
||||
function culvertReach(section: CrossSection): StructureSpan | null {
|
||||
const culvert = section.culvert;
|
||||
if (!culvert) return null;
|
||||
@@ -146,6 +161,12 @@ function culvertReach(section: CrossSection): StructureSpan | null {
|
||||
beforeM = Math.max(beforeM, span.beforeM);
|
||||
afterM = Math.max(afterM, span.afterM);
|
||||
}
|
||||
// 다단은 기준벽보다 길 수 있다(단별 수동값 — 2026-08-29). 그 몫까지 닿아야 먼
|
||||
// 측점 카드에도 그 단이 선다.
|
||||
for (const span of Object.values(section.design?.extra_spans ?? {})) {
|
||||
beforeM = Math.max(beforeM, Math.max(span.before_m, 0));
|
||||
afterM = Math.max(afterM, Math.max(span.after_m, 0));
|
||||
}
|
||||
return beforeM > 0 || afterM > 0 ? { beforeM, afterM } : null;
|
||||
}
|
||||
|
||||
@@ -247,16 +268,28 @@ function trimLinkedLayout(layout: CulvertLayout, link: CulvertLink): CulvertLayo
|
||||
return {
|
||||
...layout,
|
||||
walls: layout.walls.filter((wall) => (wall.role === "inlet" ? inletIn : outletIn)),
|
||||
// 다단(성토부)은 유출 벽에 딸린다.
|
||||
extraWalls: outletIn ? layout.extraWalls : [],
|
||||
outletFill: outletIn ? layout.outletFill : empty,
|
||||
// 다단은 **자기 구간값**으로 판정한다(2026-08-29 사용자 — 단마다 연장이 다르다).
|
||||
// 성토부선은 단이 서는 카드에서만 그린다(끝 단이 빠지면 그 아래 선도 뺀다).
|
||||
extraWalls: layout.extraWalls.filter((_wall, i) =>
|
||||
spanCovers(tierSpanOf(link.source, `extra${i}`), link.deltaM),
|
||||
),
|
||||
outletFill: spanCovers(tierSpanOf(link.source, "extra0"), link.deltaM)
|
||||
? layout.outletFill
|
||||
: empty,
|
||||
// 집수정은 **소유 측점 횡단도 하나에만** 선다(2026-08-24 사용자 확정) — 기슭막이와
|
||||
// 달리 옆 측점으로 이어지는 구조물이 아니다. 계류측 다단·성토부선도 집수정에 딸린
|
||||
// 것이라 함께 뺀다. 3D에서는 소유 측점 하나가 자기 길이(기본 2m)만큼 스윕한다.
|
||||
// 단 독립 기슭막이(관 숨김)는 집수정이 없고 이 채널이 **유입측 벽의 다단·성토부선**
|
||||
// 이라, 유출측과 같은 규칙으로 그 벽의 연장을 따른다(2026-08-29 좌우 통일).
|
||||
basinExtras: spec.hidden_pipe && inletIn ? layout.basinExtras : [],
|
||||
basinFill: spec.hidden_pipe && inletIn ? layout.basinFill : empty,
|
||||
basinExtras: spec.hidden_pipe
|
||||
? layout.basinExtras.filter((_wall, i) =>
|
||||
spanCovers(tierSpanOf(link.source, `bextra${i}`), link.deltaM),
|
||||
)
|
||||
: [],
|
||||
basinFill:
|
||||
spec.hidden_pipe && spanCovers(tierSpanOf(link.source, "bextra0"), link.deltaM)
|
||||
? layout.basinFill
|
||||
: empty,
|
||||
basin: null,
|
||||
designTrim: keepTrimSides(
|
||||
layout.designTrim,
|
||||
|
||||
@@ -564,8 +564,8 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
);
|
||||
currentCount = tiers.count;
|
||||
countValue.textContent = `${tiers.count}단`;
|
||||
// 구간값 — 있는 요소(유입·유출 기슭막이, 유입 집수정)에서만 세 행이 뜬다.
|
||||
// 다단(extra·bextra)은 소유 벽 연장을 그대로 따르는 파생물이라 대상이 아니다.
|
||||
// 구간값 — 유입·유출 기슭막이, 유입 집수정, 그리고 **다단**(2026-08-29 사용자:
|
||||
// 계곡부·능선부에서 단마다 연장이 달라 단별로 손수 넣는다)에서 세 행이 뜬다.
|
||||
currentSpan = deps.spanFor(key);
|
||||
for (const entry of spanRows) {
|
||||
entry.row.classList.toggle("is-hidden", currentSpan === null);
|
||||
|
||||
@@ -22,9 +22,13 @@ import type {
|
||||
StructureSpanControl,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
|
||||
/** 다단(추가) 벽 키인가 — 유출측 `extra0…`·유입측 `bextra0…`. */
|
||||
export const isTierKey = (key: RevetKey): boolean =>
|
||||
key.startsWith("extra") || key.startsWith("bextra");
|
||||
|
||||
/**
|
||||
* 선택된 벽이 구간값을 갖는 요소인지 — 유입·유출 기슭막이와 유입 집수정만 자기
|
||||
* 길이·전/후를 갖는다. 다단(extra·bextra)은 소유 벽 연장을 그대로 따른다.
|
||||
* 선택된 벽이 **기준벽 구간값**을 갖는 요소인지 — 유입·유출 기슭막이와 유입 집수정.
|
||||
* 다단(extra·bextra)은 자기 단별 구간값을 따로 갖는다(`isTierKey` — 2026-08-29).
|
||||
*/
|
||||
export function spanRoleOf(key: RevetKey, inletIsBasin = false): SpanRole | null {
|
||||
if (key === "inlet") return inletIsBasin ? "basin" : "inlet";
|
||||
@@ -163,13 +167,19 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
|
||||
ctx.extraWalls?.setCount(ctx.adjustChainage(), count, "basin");
|
||||
},
|
||||
equalizeExtras: () => ctx.extraWalls?.equalize(ctx.adjustChainage()),
|
||||
// 구간값(길이·전·후) — 다단은 소유 벽 연장을 따르는 파생물이라 대상이 아니다.
|
||||
// 구간값(길이·전·후). 기준벽은 배관 옵션 정본, **다단은 단별 세션 값**이다
|
||||
// (2026-08-29 사용자 — 계곡부·능선부에서 단마다 연장이 달라 수동으로 넣는다).
|
||||
// 링크 카드에서 만져도 제어기가 소유 측점 값을 고친다(원천은 하나).
|
||||
spanFor: (key) => {
|
||||
if (isTierKey(key)) return ctx.structureSpan?.tierValuesFor(ctx.section, key) ?? null;
|
||||
const role = spanRoleOf(key, ctx.inletIsBasin());
|
||||
return role && ctx.structureSpan ? ctx.structureSpan.valuesFor(ctx.section, role) : null;
|
||||
},
|
||||
setSpan: (key, patch) => {
|
||||
if (isTierKey(key)) {
|
||||
ctx.structureSpan?.updateTier(ctx.section, key, patch);
|
||||
return;
|
||||
}
|
||||
const role = spanRoleOf(key, ctx.inletIsBasin());
|
||||
if (role) ctx.structureSpan?.update(ctx.section, role, patch);
|
||||
},
|
||||
|
||||
@@ -516,6 +516,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
basinAdjustments,
|
||||
revetAdjusts: stationControls.revetAdjustsByChainage(),
|
||||
extraCounts: stationControls.extraCountsByChainage(),
|
||||
extraSpans: stationControls.extraSpansByChainage(),
|
||||
fordAdjusts: stationControls.fordAdjustsByChainage(),
|
||||
boxAdjusts: stationControls.boxAdjustsByChainage(),
|
||||
linkFlags: stationControls.linkFlagsByChainage(),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 모으기만 한다 — 판정·한계는 각 제어기가 이미 끝냈다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSectionPatch, StoredWallAdjust } from "./B06_Section_Api_Fetch";
|
||||
import type { CrossSectionPatch, StoredWallAdjust, StoredWallSpan } from "./B06_Section_Api_Fetch";
|
||||
import type { BasinAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { FordAdjust } from "./B06_Section_UI_Cross_Ford";
|
||||
import type { BoxAdjust } from "./B06_Section_UI_Cross_Box";
|
||||
@@ -22,6 +22,8 @@ export interface CrossPatchSources {
|
||||
basinAdjustments: Map<string, BasinAdjust>;
|
||||
revetAdjusts: Map<number, Record<string, StoredWallAdjust>>;
|
||||
extraCounts: Map<number, { outlet: number; basin: number }>;
|
||||
/** 다단 단별 구간값(2026-08-29) — 키는 벽 키("extra0"…/"bextra0"…). */
|
||||
extraSpans: Map<number, Record<string, StoredWallSpan>>;
|
||||
fordAdjusts: Map<number, FordAdjust>;
|
||||
boxAdjusts: Map<number, BoxAdjust>;
|
||||
linkFlags: Map<number, { detached?: boolean; followGrade?: boolean }>;
|
||||
@@ -57,6 +59,10 @@ export function buildCrossPatches(sources: CrossPatchSources): CrossSectionPatch
|
||||
sources.extraCounts.forEach((counts, chainage) => {
|
||||
patchFor(chainage).extra_wall_counts = counts;
|
||||
});
|
||||
// 다단 단별 구간값 — 세션에서 만진 값을 정본에 싣는다(2026-08-29 사용자).
|
||||
sources.extraSpans.forEach((spans, chainage) => {
|
||||
patchFor(chainage).extra_spans = spans;
|
||||
});
|
||||
// 세월교 측벽 조작값(2026-08-25) — 배수관 값과 같은 자리에 실어 3D·재계산이 잇는다.
|
||||
sources.fordAdjusts.forEach((adjust, chainage) => {
|
||||
patchFor(chainage).ford_adjust = adjust;
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
* (`_UI_Page.ts`)에서 700줄 제한으로 분리했다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossDesign, CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type {
|
||||
CrossDesign,
|
||||
CrossSection,
|
||||
SectionDetailResponse,
|
||||
StoredWallSpan,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { BasinAdjust, WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
@@ -14,6 +19,7 @@ import type {
|
||||
InletStructureControl,
|
||||
RevetLinkControl,
|
||||
RevetOffsetControl,
|
||||
SpanValues,
|
||||
StationWidthControl,
|
||||
StructureSpanControl,
|
||||
} from "./B06_Section_UI_Cross_View";
|
||||
@@ -36,6 +42,7 @@ export interface StationControlDeps {
|
||||
| "inletstruct"
|
||||
| "basinadjust"
|
||||
| "extrawall"
|
||||
| "extraspan"
|
||||
| "revetlink"
|
||||
| "fordadjust"
|
||||
| "boxadjust",
|
||||
@@ -79,6 +86,8 @@ export interface StationControls {
|
||||
extraCountsByChainage: () => Map<number, { outlet: number; basin: number }>;
|
||||
/** 확정 payload용 — 측점별 연동 해제·종단경사 반영(2026-08-24). */
|
||||
linkFlagsByChainage: () => Map<number, { detached?: boolean; followGrade?: boolean }>;
|
||||
/** 다단 단별 구간값(2026-08-29) — 확정·임시저장 payload용. */
|
||||
extraSpansByChainage: () => Map<number, Record<string, StoredWallSpan>>;
|
||||
/** 예약된 구간값 저장을 즉시 내보낸다 — 확정·임시저장 직전에 부른다. */
|
||||
flushCulvertOptions: () => Promise<void>;
|
||||
load: () => void;
|
||||
@@ -416,11 +425,13 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
if (count === previous) return;
|
||||
if (count === 0) extraCounts.delete(key);
|
||||
else extraCounts.set(key, count);
|
||||
// 줄어든 단의 이동량은 지운다 — 다시 늘리면 자동 자리에서 시작한다.
|
||||
// 줄어든 단의 이동량·구간값은 지운다 — 다시 늘리면 기본값에서 시작한다.
|
||||
for (let i = count; i < previous; i += 1) {
|
||||
revetShifts.delete(revetKey(chainageM, `${extraPrefix(side)}${i}` as RevetKey));
|
||||
extraSpans.delete(spanKeyOf(chainageM, `${extraPrefix(side)}${i}`));
|
||||
}
|
||||
persistRevetShifts();
|
||||
persistExtraSpans();
|
||||
persistExtraCounts();
|
||||
patchCachedDesign(chainageM, {
|
||||
extra_wall_counts: {
|
||||
@@ -439,8 +450,10 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
else extraCounts.set(key, built);
|
||||
for (let i = built; i < count; i += 1) {
|
||||
revetShifts.delete(revetKey(chainageM, `${extraPrefix(side)}${i}` as RevetKey));
|
||||
extraSpans.delete(spanKeyOf(chainageM, `${extraPrefix(side)}${i}`));
|
||||
}
|
||||
persistRevetShifts();
|
||||
persistExtraSpans();
|
||||
persistExtraCounts();
|
||||
},
|
||||
};
|
||||
@@ -503,8 +516,97 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
return culvertOwnerFor(section, sections) ?? null;
|
||||
};
|
||||
|
||||
/* ── 다단(추가) 기슭막이 **단별** 구간값(2026-08-29 사용자) ───────────────
|
||||
* 기준벽 연장에 종속시키지 않는다 — 계곡부·능선부에서 아래 단일수록 연장이 달라져
|
||||
* 자동 규칙으로 못 잡는다. 제어는 세션이고 [저장]·[확정] 때 정본으로 간다
|
||||
* (4축 조작값과 같은 경로). 키는 `누가거리:벽키`(예 `234.10:extra0`). */
|
||||
const extraSpans = new Map<string, SpanValues>();
|
||||
const extraSpanSessionKey = (): string | null => deps.sessionKey("extraspan");
|
||||
const spanKeyOf = (chainageM: number, wall: string): string => `${chainageM.toFixed(2)}:${wall}`;
|
||||
|
||||
function loadExtraSpans(): void {
|
||||
extraSpans.clear();
|
||||
const key = extraSpanSessionKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(key);
|
||||
if (!raw) return;
|
||||
const parsed = JSON.parse(raw) as Record<string, SpanValues>;
|
||||
Object.entries(parsed).forEach(([mapKey, value]) => {
|
||||
if (value && Number.isFinite(value.beforeM) && Number.isFinite(value.afterM)) {
|
||||
extraSpans.set(mapKey, value);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
/* 손상된 세션 값은 무시 — 기본 구간값으로 재시작. */
|
||||
}
|
||||
}
|
||||
|
||||
function persistExtraSpans(): void {
|
||||
const key = extraSpanSessionKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(Object.fromEntries(extraSpans)));
|
||||
} catch {
|
||||
/* 세션 저장 실패는 무시 — 값은 메모리에 유지된다. */
|
||||
}
|
||||
}
|
||||
|
||||
/** 정본(design)에 남은 단별 구간값 — 세션에 없을 때의 다음 후보. */
|
||||
const storedTierSpan = (owner: CrossSection, wall: string): SpanValues | null => {
|
||||
const stored = owner.design?.extra_spans?.[wall];
|
||||
if (!stored) return null;
|
||||
return {
|
||||
lengthM: stored.length_m,
|
||||
beforeM: stored.before_m,
|
||||
afterM: stored.after_m,
|
||||
};
|
||||
};
|
||||
|
||||
/** 이 측점의 단별 구간값 전부(세션 우선) — 캐시·payload에 실을 모양으로. */
|
||||
const tierSpansOf = (owner: CrossSection): Record<string, StoredWallSpan> => {
|
||||
const prefix = `${owner.chainage_m.toFixed(2)}:`;
|
||||
const result: Record<string, StoredWallSpan> = { ...(owner.design?.extra_spans ?? {}) };
|
||||
extraSpans.forEach((value, key) => {
|
||||
if (!key.startsWith(prefix)) return;
|
||||
result[key.slice(prefix.length)] = {
|
||||
length_m: value.lengthM,
|
||||
before_m: value.beforeM,
|
||||
after_m: value.afterM,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const structureSpanControl: StructureSpanControl = {
|
||||
ownerOf,
|
||||
tierValuesFor: (section, key) => {
|
||||
const owner = ownerOf(section) ?? section;
|
||||
return (
|
||||
extraSpans.get(spanKeyOf(owner.chainage_m, key)) ??
|
||||
storedTierSpan(owner, key) ??
|
||||
CulvertConst.EXTRA_SPAN_DEFAULT
|
||||
);
|
||||
},
|
||||
updateTier: (section, key, patch) => {
|
||||
const owner = ownerOf(section);
|
||||
if (!owner) return;
|
||||
const mapKey = spanKeyOf(owner.chainage_m, key);
|
||||
const current =
|
||||
extraSpans.get(mapKey) ?? storedTierSpan(owner, key) ?? CulvertConst.EXTRA_SPAN_DEFAULT;
|
||||
const next = CulvertConst.applySpanPatch(current, patch);
|
||||
extraSpans.set(mapKey, next);
|
||||
persistExtraSpans();
|
||||
// 캐시(design)에도 얹는다 — 링크 판정·3D가 순수 함수로 이 값을 읽는다.
|
||||
patchCachedDesign(owner.chainage_m, { extra_spans: tierSpansOf(owner) });
|
||||
// 연장이 바뀌면 링크되는 옆 측점이 달라진다 — 옛 연장·새 연장을 합친 구간만.
|
||||
const reach = Math.max(current.beforeM, current.afterM, next.beforeM, next.afterM);
|
||||
for (const other of deps.detail()?.cross_sections ?? []) {
|
||||
if (Math.abs(other.chainage_m - owner.chainage_m) <= reach + 1e-9) {
|
||||
deps.refreshCard(other.chainage_m);
|
||||
}
|
||||
}
|
||||
},
|
||||
valuesFor: (section, role) => {
|
||||
const owner = ownerOf(section);
|
||||
return owner ? CulvertConst.spanValuesOf(owner, role) : null;
|
||||
@@ -514,20 +616,8 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
if (!owner?.culvert) return;
|
||||
const current = CulvertConst.spanValuesOf(owner, role);
|
||||
if (!current) return;
|
||||
// 길이를 바꾸면 **늘어난 몫만** 지금 비율대로 나눠 담는다 — 매번 총길이에서
|
||||
// 비율로 다시 계산하면 0.1m 반올림이 쌓여 5.0/5.0이 5.6/5.4로 어긋난다
|
||||
// (2026-08-24 화면 실측). 전·후를 바꾸면 길이는 둘의 합이다.
|
||||
let { beforeM, afterM } = current;
|
||||
if (patch.lengthM !== undefined) {
|
||||
const total = Math.max(patch.lengthM, 0);
|
||||
const delta = total - current.lengthM;
|
||||
const ratio = current.lengthM > 1e-9 ? current.beforeM / current.lengthM : 0.5;
|
||||
beforeM = Math.max(Math.round((current.beforeM + delta * ratio) * 10) / 10, 0);
|
||||
afterM = Math.max(Math.round((total - beforeM) * 10) / 10, 0);
|
||||
}
|
||||
if (patch.beforeM !== undefined) beforeM = Math.max(patch.beforeM, 0);
|
||||
if (patch.afterM !== undefined) afterM = Math.max(patch.afterM, 0);
|
||||
const lengthM = Math.round((beforeM + afterM) * 10) / 10;
|
||||
// 길이↔전/후 산식은 다단과 공용(`applySpanPatch`).
|
||||
const { lengthM, beforeM, afterM } = CulvertConst.applySpanPatch(current, patch);
|
||||
const spec = role === "outlet" ? owner.culvert.outlet : owner.culvert.inlet;
|
||||
const keys = CulvertConst.SPAN_OPTION_KEYS[role];
|
||||
if (role === "basin") {
|
||||
@@ -664,6 +754,18 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
});
|
||||
return result;
|
||||
},
|
||||
extraSpansByChainage: () => {
|
||||
const result = new Map<number, Record<string, StoredWallSpan>>();
|
||||
extraSpans.forEach((value, key) => {
|
||||
const [chainage, wall] = key.split(":");
|
||||
const chainageM = Number(chainage);
|
||||
if (!Number.isFinite(chainageM) || !wall) return;
|
||||
const bucket = result.get(chainageM) ?? {};
|
||||
bucket[wall] = { length_m: value.lengthM, before_m: value.beforeM, after_m: value.afterM };
|
||||
result.set(chainageM, bucket);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
flushCulvertOptions: () => culvertOptions.flush(),
|
||||
load: () => {
|
||||
loadStationWidths();
|
||||
@@ -671,6 +773,7 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
loadInletStructures();
|
||||
loadBasinAdjustments();
|
||||
loadExtraCounts();
|
||||
loadExtraSpans();
|
||||
loadLinkFlags();
|
||||
bodyControls.ford.load();
|
||||
bodyControls.box.load();
|
||||
|
||||
Reference in New Issue
Block a user