feat(B06): 집수정 좌우·상하 이동 및 내공 조정 + 설계선 스냅 급경사 제거
- 집수정 조정값(BasinAdjust: 내공 폭·높이, 좌우 lateralM, 상하 slopeM) 신설. 측점별 세션 보관 + 백엔드 design.basin_adjust 저장 경로(Schema·Router) 연동 - 조정창에 집수정 조정 항목 추가, I형 관 시작점을 벽 안쪽 변과 원지반의 교차점으로 산출(이분법), 집수정 성토(되메움)선 5° → 성토 비탈 1:1.2로 통일 - 설계선 트림 수정: 배수관 레이아웃이 사면 폴리라인을 준 쪽은 그 시작점(노견)에서 설계선을 끊는다 — 종전에는 트림 경계에서 끝점 표고를 벽 상단으로 강제 스냅해 집수정을 좌우로 옮기면 마지막 구간이 수직으로 튀는 선이 남았다(사용자 지적) - basinApproachPoints의 길이 0 중복 꼭짓점 제거 검증: 4+4.3(집수정 I형 좌우 2.9m 이동)·13+15.7·5+0.0·2+0.0에서 급경사 세그먼트 0건 · tsc --noEmit 통과 · ruff check 통과 · prettier 적용 · pytest tmp/tests/ 148 passed, 7 skipped · B06_Section 700줄 초과 0건. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,6 +244,13 @@ export type DitchType = "standard" | "l_type";
|
||||
|
||||
/** 측점 표준횡단 설계 계산 결과(잠정치). data.design에 저장되는 구조와 동일. */
|
||||
export interface CrossDesign {
|
||||
inlet_structure?: "auto" | "revet" | "I" | "L" | "U";
|
||||
basin_adjust?: {
|
||||
innerWidthM: number;
|
||||
innerHeightM: number;
|
||||
lateralM: number;
|
||||
slopeM: number;
|
||||
};
|
||||
ground_type: GroundType;
|
||||
geometry_preset: "soil" | "rock";
|
||||
section_mode: SectionMode;
|
||||
@@ -333,6 +340,13 @@ export interface CrossSectionPatch {
|
||||
rock_boundary_offset_m?: number;
|
||||
/** 측점 개별 표시 반폭(m, 2026-08-06 사용자 지시). */
|
||||
display_half_width_m?: number;
|
||||
inlet_structure?: "auto" | "revet" | "I" | "L" | "U";
|
||||
basin_adjust?: {
|
||||
innerWidthM: number;
|
||||
innerHeightM: number;
|
||||
lateralM: number;
|
||||
slopeM: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 공통 fetch 헬퍼: 타임아웃 + 인증 헤더 + 오류 응답 변환. */
|
||||
|
||||
@@ -640,11 +640,10 @@ async def compute_cross_section_design(
|
||||
for record in stored_designs:
|
||||
if abs(float(record["chainage_m"]) - request.chainage_m) < 0.01:
|
||||
stored_design = record.get("design")
|
||||
if (
|
||||
isinstance(stored_design, dict)
|
||||
and stored_design.get("display_half_width_m") is not None
|
||||
):
|
||||
design["display_half_width_m"] = stored_design["display_half_width_m"]
|
||||
if isinstance(stored_design, dict):
|
||||
for key in ("display_half_width_m", "inlet_structure", "basin_adjust"):
|
||||
if stored_design.get(key) is not None:
|
||||
design[key] = stored_design[key]
|
||||
break
|
||||
await connection.begin()
|
||||
try:
|
||||
|
||||
@@ -79,6 +79,10 @@ async def _apply_section_edits(
|
||||
patch["rock_boundary_offset_m"] = patch_item.rock_boundary_offset_m
|
||||
if patch_item.display_half_width_m is not None:
|
||||
patch["display_half_width_m"] = patch_item.display_half_width_m
|
||||
if patch_item.inlet_structure is not None:
|
||||
patch["inlet_structure"] = patch_item.inlet_structure
|
||||
if patch_item.basin_adjust is not None:
|
||||
patch["basin_adjust"] = patch_item.basin_adjust.model_dump()
|
||||
if patch:
|
||||
await merge_cross_section_design_patch(
|
||||
connection, route_id=route_id, chainage_m=patch_item.chainage_m, patch=patch
|
||||
|
||||
@@ -136,4 +136,8 @@ def recompute_designs_for_alignment(
|
||||
design.update(status="provisional", pavement_suggested=suggested)
|
||||
if stored.get("display_half_width_m") is not None:
|
||||
design["display_half_width_m"] = stored["display_half_width_m"]
|
||||
if stored.get("inlet_structure") is not None:
|
||||
design["inlet_structure"] = stored["inlet_structure"]
|
||||
if stored.get("basin_adjust") is not None:
|
||||
design["basin_adjust"] = stored["basin_adjust"]
|
||||
section["design"] = design
|
||||
|
||||
@@ -66,6 +66,15 @@ class CrossDesignResponse(BaseModel):
|
||||
design: dict[str, Any]
|
||||
|
||||
|
||||
class BasinAdjustPatch(BaseModel):
|
||||
"""집수정 내부 치수와 기본 위치 대비 이동량."""
|
||||
|
||||
innerWidthM: float = Field(..., ge=1.0, le=2.0)
|
||||
innerHeightM: float = Field(..., ge=1.0, le=2.0)
|
||||
lateralM: float = Field(..., ge=0.0, le=10.0)
|
||||
slopeM: float = Field(..., ge=0.0, le=10.0)
|
||||
|
||||
|
||||
class CrossSectionPatch(BaseModel):
|
||||
"""확정 시 측점별 data.design에 병합할 프론트 세션 보관값."""
|
||||
|
||||
@@ -74,6 +83,8 @@ class CrossSectionPatch(BaseModel):
|
||||
rock_boundary_offset_m: float | None = None
|
||||
# 측점별 표시 반폭(m) — 카드 개별 조절값. 전역 반폭과 다를 때만 실린다(2026-08-06).
|
||||
display_half_width_m: float | None = Field(default=None, gt=0)
|
||||
inlet_structure: Literal["auto", "revet", "I", "L", "U"] | None = None
|
||||
basin_adjust: BasinAdjustPatch | None = None
|
||||
|
||||
|
||||
class SectionConfirmRequest(BaseModel):
|
||||
|
||||
@@ -254,8 +254,7 @@ export function appendCulvertOverlay(
|
||||
layer.append(cut);
|
||||
}
|
||||
if (basin.fillLine) {
|
||||
// 구조물이 원지반 밖으로 나온 경우의 성토(되메움) 계획선 — 경사 5도(임시값,
|
||||
// 2026-08-22 사용자 지정).
|
||||
// 구조물이 원지반 밖으로 나온 경우의 성토(되메움) 계획선 — 성토 비탈 1:1.2.
|
||||
const fill = document.createElementNS(SVG_NS, "line");
|
||||
fill.setAttribute("x1", String(x(basin.fillLine.from.offset)));
|
||||
fill.setAttribute("y1", String(toDisplayY(basin.fillLine.from.elevation)));
|
||||
@@ -266,7 +265,7 @@ export function appendCulvertOverlay(
|
||||
fillTitle.textContent =
|
||||
basin.shape === "I"
|
||||
? "집수정(I형) 되메움(성토)선 — 관 하단에서 3도(계류 쪽 오름)로 원지반과 연결"
|
||||
: `집수정(${shapeName}) 성토(되메움)선 — 구조물이 원지반 밖으로 나와 5° 경사로 채움`;
|
||||
: `집수정(${shapeName}) 성토(되메움)선 — 구조물이 원지반 밖으로 나와 1:1.2로 채움`;
|
||||
fill.append(fillTitle);
|
||||
layer.append(fill);
|
||||
}
|
||||
|
||||
@@ -22,11 +22,50 @@ import {
|
||||
import { minShoulderWallOffset, slopeToeOffset } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import type {
|
||||
BasinLayout,
|
||||
BasinAdjust,
|
||||
BasinShape,
|
||||
EndFace,
|
||||
OffsetPoint,
|
||||
InletStructureChoice,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
export function resolveBasinChoice(
|
||||
choice: InletStructureChoice,
|
||||
ruleReason: BasinLayout["reason"] | null,
|
||||
): { reason: BasinLayout["reason"] | null; shape: BasinShape } {
|
||||
const selected = choice === "I" || choice === "L" || choice === "U";
|
||||
return {
|
||||
reason: choice === "revet" ? null : selected ? (ruleReason ?? "manual") : ruleReason,
|
||||
shape: selected ? choice : "L",
|
||||
};
|
||||
}
|
||||
|
||||
export function basinApproachPoints(
|
||||
edge: { offset_m: number; elevation_m: number },
|
||||
outward: number,
|
||||
adjust: BasinAdjust,
|
||||
trim: OffsetPoint,
|
||||
): OffsetPoint[] | null {
|
||||
// 접근선은 **설계선이 벽 상단까지 따라올 경로**다. 이 선을 주지 않으면 설계선이
|
||||
// 자기 성토 물매(1:1.2)로 내려갔다가 트림 표고(벽 상단)로 되튀어 올라 급경사
|
||||
// 선이 남는다(2026-08-22 사용자 지적). 좌우만 옮겨도 반드시 준다 — 집수정이
|
||||
// 나간 만큼 노견이 수평으로 연장되는 것이 맞는 표현이다.
|
||||
if (adjust.lateralM <= 1e-6 && adjust.slopeM <= 1e-6) return null;
|
||||
const points: OffsetPoint[] = [{ offset: edge.offset_m, elevation: edge.elevation_m }];
|
||||
if (adjust.lateralM > 1e-6) {
|
||||
points.push({
|
||||
offset: edge.offset_m + outward * adjust.lateralM,
|
||||
elevation: edge.elevation_m,
|
||||
});
|
||||
}
|
||||
// 중복 꼭짓점 제거 — 수평 선반 끝과 벽 상단이 겹치면 길이 0 구간이 남는다.
|
||||
const last = points[points.length - 1];
|
||||
if (Math.hypot(trim.offset - last.offset, trim.elevation - last.elevation) > 1e-6) {
|
||||
points.push(trim);
|
||||
}
|
||||
return points.length >= 2 ? points : null;
|
||||
}
|
||||
|
||||
/** 집수정 구성 입력 — 기하 본체가 자기 상태를 명시적으로 물려 준다. */
|
||||
export interface BasinBuildInput {
|
||||
/** 관 유입 기준점(노견 끝 offset, invert 표고 — ㄴ·ㄷ형은 상승분 반영 후). */
|
||||
@@ -42,6 +81,7 @@ export interface BasinBuildInput {
|
||||
groundAt: (offset: number) => number;
|
||||
/** 표준단면 절토 경사(1:n) — 절토 계획선용. */
|
||||
cutSlopeRatio: number;
|
||||
adjust: BasinAdjust;
|
||||
}
|
||||
|
||||
/** 집수정 구성 결과 — 본체가 트림·관 끝단면·관 시작점에 나눠 꽂는다. */
|
||||
@@ -58,21 +98,35 @@ export interface BasinBuildResult {
|
||||
|
||||
/** 실무 내공 폭(m) — 울진 돌집수정. I형 관 시작점(노견+1.0m) 계산에도 쓴다. */
|
||||
export const BASIN_INNER_WIDTH_M = 1.0;
|
||||
const INNER_WIDTH_M = BASIN_INNER_WIDTH_M;
|
||||
|
||||
/** 집수정 주변 성토(되메움) 표면 경사(도) — 2026-08-22 사용자 지정 "일단 5도". */
|
||||
const BASIN_FILL_ANGLE_DEG = 5;
|
||||
/** 집수정 주변 성토(되메움) 기울기(1:n) — 성토 비탈 기본 1:1.2. */
|
||||
const BASIN_FILL_SLOPE_RATIO = 1.2;
|
||||
|
||||
/** 집수정 단면을 만든다. 형태별 규칙은 파일 머리말 참조. */
|
||||
export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
const { anchor, outward, shape, reason, diameterM, edge, groundAt, cutSlopeRatio } = input;
|
||||
const {
|
||||
anchor: baseAnchor,
|
||||
outward,
|
||||
shape,
|
||||
reason,
|
||||
diameterM,
|
||||
edge,
|
||||
groundAt,
|
||||
cutSlopeRatio,
|
||||
adjust,
|
||||
} = input;
|
||||
const movedAnchor = {
|
||||
offset: baseAnchor.offset + outward * (adjust.lateralM + adjust.slopeM),
|
||||
elevation: baseAnchor.elevation - adjust.slopeM / 1.2,
|
||||
};
|
||||
const anchor = movedAnchor;
|
||||
const memberT = BASIN_MEMBER_THICKNESS_M;
|
||||
const floorThickness = memberT;
|
||||
// 벽 높이: I형은 노견 접점까지 성장, ㄴ·ㄷ형은 내부 높이 1.2m 고정(임시값).
|
||||
const wallHeight =
|
||||
shape === "I"
|
||||
? Math.max(diameterM + REVET_FREEBOARD_M, edge.elevation_m - anchor.elevation)
|
||||
: BASIN_INNER_HEIGHT_M;
|
||||
: adjust.innerHeightM;
|
||||
// 기움은 **수직 기준 반전** — 상단이 도로측(내공 쪽)으로 1:0.3 물러난다(2026-08-20).
|
||||
// bottomElevation을 내리면 면 기울기를 그대로 연장한다(I형 근입용).
|
||||
const defaultBottom = anchor.elevation - floorThickness;
|
||||
@@ -112,6 +166,26 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
{ kind: "wall", points: wallOf(wallBase, outward, iWallBottom) },
|
||||
];
|
||||
const iWallPoints = parts[0].points;
|
||||
const iGroundIntersection = (): OffsetPoint => {
|
||||
const low = iWallPoints[3];
|
||||
const high = iWallPoints[2];
|
||||
let lo = 0;
|
||||
let hi = 1;
|
||||
for (let pass = 0; pass < 32; pass += 1) {
|
||||
const mid = (lo + hi) / 2;
|
||||
const point = {
|
||||
offset: low.offset + (high.offset - low.offset) * mid,
|
||||
elevation: low.elevation + (high.elevation - low.elevation) * mid,
|
||||
};
|
||||
if (point.elevation < groundAt(point.offset)) lo = mid;
|
||||
else hi = mid;
|
||||
}
|
||||
const t = (lo + hi) / 2;
|
||||
return {
|
||||
offset: low.offset + (high.offset - low.offset) * t,
|
||||
elevation: low.elevation + (high.elevation - low.elevation) * t,
|
||||
};
|
||||
};
|
||||
// ㄴ형 = I형 + 바닥판. 바닥은 벽 바깥(계류측) 변에 맞대고 그 너머로 뻗는다
|
||||
// (2026-08-20 확정). 벽이 기울어 있어 안쪽 변도 벽 바깥면 선을 그대로 따른다.
|
||||
const wallOuterAt = (elevation: number): number => {
|
||||
@@ -121,7 +195,7 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
};
|
||||
const floorTopInner = wallOuterAt(anchor.elevation);
|
||||
const floorBottomInner = wallOuterAt(anchor.elevation - floorThickness);
|
||||
const floorOuter = floorTopInner + outward * (INNER_WIDTH_M + memberT);
|
||||
const floorOuter = floorTopInner + outward * (adjust.innerWidthM + memberT);
|
||||
const floorTaper = REVET_LEAN_RATIO * floorThickness;
|
||||
if (shape !== "I") {
|
||||
parts.push({
|
||||
@@ -147,7 +221,7 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
// 구조물 계류측 끝(ㄴ형 = 바닥 바깥 끝 상단, ㄷ형 = 막음벽 계류측 상단 꼭짓점)과
|
||||
// 원지반의 관계(2026-08-22 사용자 ①):
|
||||
// · 원지반 안쪽에 박히면 → 표준단면 절토경사(1:n)로 **절토선**
|
||||
// · 원지반 밖으로 나오면 → **성토(되메움)선** — 경사 5도(임시값)로 지반까지.
|
||||
// · 원지반 밖으로 나오면 → **성토(되메움)선** — 1:1.2로 지반까지.
|
||||
let basinCut: BasinLayout["cutLine"] = null;
|
||||
let basinFill: BasinLayout["cutLine"] = null;
|
||||
if (shape !== "I") {
|
||||
@@ -167,8 +241,11 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (groundHere < refTop.elevation - 0.05) {
|
||||
const dropPerM = Math.tan((BASIN_FILL_ANGLE_DEG * Math.PI) / 180);
|
||||
} else if (
|
||||
groundHere < refTop.elevation - 0.05 &&
|
||||
!(adjust.lateralM > 1e-6 && adjust.slopeM <= 1e-6)
|
||||
) {
|
||||
const dropPerM = 1 / BASIN_FILL_SLOPE_RATIO;
|
||||
for (let run = 0.05; run <= 30; run += 0.05) {
|
||||
const probe = refTop.offset + outward * run;
|
||||
const elevation = refTop.elevation - dropPerM * run;
|
||||
@@ -203,7 +280,7 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
},
|
||||
pipeEnd:
|
||||
shape === "I"
|
||||
? { offset: anchor.offset + outward * INNER_WIDTH_M, elevation: anchor.elevation }
|
||||
? iGroundIntersection()
|
||||
: { offset: floorTopInner, elevation: anchor.elevation },
|
||||
trimOffset: wallBase - outward * REVET_LEAN_RATIO * wallHeight,
|
||||
trimElevation: anchor.elevation + wallHeight,
|
||||
@@ -248,6 +325,12 @@ export function inletChoiceAvailability(input: {
|
||||
edge,
|
||||
groundAt,
|
||||
cutSlopeRatio: input.cutSlopeRatio,
|
||||
adjust: {
|
||||
innerWidthM: BASIN_INNER_WIDTH_M,
|
||||
innerHeightM: BASIN_INNER_HEIGHT_M,
|
||||
lateralM: 0,
|
||||
slopeM: 0,
|
||||
},
|
||||
});
|
||||
return { revetAllowed, basinLUAllowed: probe.basin.cutLine != null, revetAutoOffset };
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ export function revetHeightLimit(form: string | null | undefined): number {
|
||||
|
||||
/* ── 기슭막이 재질·높이 조작(2026-08-22 사용자 확정) ─────────────────────
|
||||
* 높이 조작이 교본 형태별 한계와 부딪히면 **재질을 바꿔야** 더 올릴 수 있다:
|
||||
* 메쌓기 2.0 / 찰쌓기 3.0(돌쌓기.md §1) / 콘크리트 5.0(임시 — 사용자 지정
|
||||
* "일단 5m", 흙막이.md 산복기초 4.0과 다름·확정 시 교체). 높이 기준은 근입
|
||||
* 메쌓기 2.0 / 찰쌓기 3.0(돌쌓기.md §1) / 콘크리트 5.0(프로젝트 기본값,
|
||||
* 외부 기준 미확인). 높이 기준은 근입
|
||||
* 0.5m 위 기준선(관 invert 자리)~상단의 **계산용 높이**로 종전과 동일. */
|
||||
export type RevetMaterial = "dry" | "wet" | "concrete";
|
||||
|
||||
@@ -118,7 +118,7 @@ export const PIPE_CONNECT_GRADE = Math.tan((3 * Math.PI) / 180);
|
||||
export const PIPE_WALL_MIN_HEIGHT_M = 2.0;
|
||||
/** 배관 기슭막이 기본 높이(m) — 재질과 무관하게 2.0(2026-08-22 사용자 정정). */
|
||||
export const PIPE_WALL_DEFAULT_HEIGHT_M = 2.0;
|
||||
/** 일반(추가) 기슭막이 높이 조작 하한(m). */
|
||||
/** 일반(추가) 기슭막이 높이 조작 하한(m) — 프로젝트 기본값 0.5m. */
|
||||
export const EXTRA_WALL_MIN_HEIGHT_M = 0.5;
|
||||
/** 일반(추가) 기슭막이 기본 높이(m). */
|
||||
export const EXTRA_WALL_DEFAULT_HEIGHT_M = 1.5;
|
||||
@@ -161,7 +161,7 @@ export const BASIN_BOTTOM_CLEARANCE_M = 0.5;
|
||||
|
||||
/**
|
||||
* 집수정 내부 높이(m) — 바닥판이 있는 ㄴ(L)·ㄷ형의 **바닥 윗면~벽 상단** 깊이.
|
||||
* ⚠ 임시 설정값 1.2(2026-08-22 사용자 지정 "일단 1.2로") — 근거 확정 시 교체.
|
||||
* 프로젝트 기본값 1.2m(2026-08-22 사용자 확정). 외부 기준은 확인되지 않았다.
|
||||
* I형(공유벽뿐)은 이 값을 쓰지 않고 노견 끝 접점까지 벽을 키운다.
|
||||
*/
|
||||
export const BASIN_INNER_HEIGHT_M = 1.2;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
BASIN_INNER_HEIGHT_M,
|
||||
BASIN_MAX_FILL_SLOPE_M,
|
||||
FILL_MIN_RISE_M,
|
||||
materialFromForm,
|
||||
@@ -32,7 +31,7 @@ import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type {
|
||||
BasinLayout,
|
||||
BasinShape,
|
||||
BasinAdjust,
|
||||
CulvertLayout,
|
||||
EndFace,
|
||||
InletStructureChoice,
|
||||
@@ -41,6 +40,7 @@ import type {
|
||||
WallAdjust,
|
||||
WallLayout,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { DEFAULT_BASIN_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
// 상수·자료형은 분리 파일에 있고, 기존 import 경로를 유지하기 위해 그대로 다시 내보낸다.
|
||||
export * from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
@@ -48,9 +48,11 @@ export * from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
import {
|
||||
applyBasinPipeFill,
|
||||
basinApproachPoints,
|
||||
BASIN_INNER_WIDTH_M,
|
||||
buildBasin,
|
||||
inletChoiceAvailability,
|
||||
resolveBasinChoice,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Basin";
|
||||
import { buildOutletExtras, inletGroundConnector } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
import type { FillSlopeSegment, WallVertical } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
@@ -76,9 +78,8 @@ export function computeCulvertLayout(
|
||||
groundSamples: SectionSample[],
|
||||
/** 사용자 조작값(좌우 x·상하 d·높이 h·재질 m — 2026-08-22 4축). 없으면 자동. */
|
||||
revetShift?: { inlet?: WallAdjust; outlet?: WallAdjust; extras?: WallAdjust[] },
|
||||
/** 유입측 구조물 형식 선택(드롭다운) — 없으면 auto(규칙). */
|
||||
inletStructure?: InletStructureChoice,
|
||||
/** 다단 등간격 배치 1회성 트리거(2026-08-22 사용자 ①). */
|
||||
basinAdjustment?: BasinAdjust,
|
||||
equalizeExtras?: boolean,
|
||||
): CulvertLayout | null {
|
||||
const culvert = section.culvert;
|
||||
@@ -92,6 +93,7 @@ export function computeCulvertLayout(
|
||||
const minSample = Math.min(...sampleOffsets);
|
||||
const maxSample = Math.max(...sampleOffsets);
|
||||
if (!(maxSample > minSample)) return null;
|
||||
const basinAdjust = basinAdjustment ?? DEFAULT_BASIN_ADJUST;
|
||||
|
||||
// 좌표 규약: +offset = 좌측. 상단측 = 유입(미상이면 좌측 폴백).
|
||||
const uphill = section.uphill_side ?? "left";
|
||||
@@ -132,24 +134,16 @@ export function computeCulvertLayout(
|
||||
: inletFillSlopeLen <= BASIN_MAX_FILL_SLOPE_M + 1e-6
|
||||
? "short"
|
||||
: null;
|
||||
// 사용자 선택(드롭다운 — 2026-08-22)이 규칙보다 우선한다:
|
||||
// revet = 기슭막이+배관 강제(양측성토 로직), I/L/U = 해당 형식 집수정 강제.
|
||||
const choice: InletStructureChoice = inletStructure ?? "auto";
|
||||
const basinReason: BasinLayout["reason"] | null =
|
||||
choice === "revet"
|
||||
? null
|
||||
: choice === "I" || choice === "L" || choice === "U"
|
||||
? (ruleReason ?? "manual")
|
||||
: ruleReason;
|
||||
// 집수정 형식 기본값 = ㄴ(L)형(2026-08-20 확정) — 선택 시 그 형식.
|
||||
const basinShape: BasinShape = choice === "I" || choice === "L" || choice === "U" ? choice : "L";
|
||||
const { reason: basinReason, shape: basinShape } = resolveBasinChoice(choice, ruleReason);
|
||||
// ㄴ·ㄷ형은 사이즈 유지한 채 통째로 노견 끝점 일치(2026-08-22 확정) — 바닥(=관
|
||||
// 유입 invert)이 따라 올라 물매도 바뀐다. I형은 원지반 배관 유지(상세 Basin 참조).
|
||||
if (basinReason && basinShape !== "I") {
|
||||
inlet.elevation = inletInfo.edge.elevation_m - BASIN_INNER_HEIGHT_M;
|
||||
inlet.elevation = inletInfo.edge.elevation_m - basinAdjust.innerHeightM;
|
||||
} else if (basinReason) {
|
||||
// I형 — 관 시작점(내공 1.0m 자리) invert = 그 x의 원지반(토피 상한 이내).
|
||||
const startOffset = inlet.offset + inletInfo.outward * BASIN_INNER_WIDTH_M;
|
||||
const startOffset =
|
||||
inlet.offset + inletInfo.outward * (BASIN_INNER_WIDTH_M + basinAdjust.lateralM);
|
||||
inlet.elevation = Math.min(groundAt(startOffset), invertCap(inletInfo.edge));
|
||||
}
|
||||
|
||||
@@ -305,6 +299,7 @@ export function computeCulvertLayout(
|
||||
edge: spec.role === "inlet" ? inletInfo.edge : outletInfo.edge,
|
||||
groundAt,
|
||||
cutSlopeRatio: section.design?.cut_slope_ratio ?? 1.0,
|
||||
adjust: basinAdjust,
|
||||
});
|
||||
basin = built.basin;
|
||||
basinPipeEnd = built.pipeEnd;
|
||||
@@ -317,6 +312,14 @@ export function computeCulvertLayout(
|
||||
trimMin = built.trimOffset;
|
||||
trimMinElevation = built.trimElevation;
|
||||
}
|
||||
const points = basinApproachPoints(inletInfo.edge, outward, basinAdjust, {
|
||||
offset: built.trimOffset,
|
||||
elevation: built.trimElevation,
|
||||
});
|
||||
if (points) {
|
||||
if (outward > 0) trimMaxSlope = { points };
|
||||
else trimMinSlope = { points };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// 형상(사용자 스케치 확정 — 좌측 벽 기준, 우측은 반전): 배면(도로측) 수직, 전면
|
||||
@@ -611,7 +614,7 @@ export function computeCulvertLayout(
|
||||
}
|
||||
|
||||
// I형 집수정: 관 하단 꼭짓점이 지반 위에 뜨면 수평 되메움선(Basin 분리 — 700줄).
|
||||
if (basin) {
|
||||
if (basin && !(basinAdjust.lateralM > 1e-6 && basinAdjust.slopeM <= 1e-6)) {
|
||||
applyBasinPipeFill(basin, pipeCorners.inlet.bottom, inletInfo.outward, groundAt);
|
||||
}
|
||||
// 유입 기슭막이의 관 시작 접속선(2026-08-22 ②) — 위 0도 성토선 / 아래 0도 1m+절토선.
|
||||
|
||||
@@ -105,6 +105,21 @@ export interface BasinLayout {
|
||||
fillLine: { from: OffsetPoint; to: OffsetPoint } | null;
|
||||
}
|
||||
|
||||
/** 측점별 집수정 조정값. 위치값은 기본 배치 기준이며 +는 계류측/사면 아래 방향이다. */
|
||||
export interface BasinAdjust {
|
||||
innerWidthM: number;
|
||||
innerHeightM: number;
|
||||
lateralM: number;
|
||||
slopeM: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_BASIN_ADJUST: BasinAdjust = {
|
||||
innerWidthM: 1.0,
|
||||
innerHeightM: 1.2,
|
||||
lateralM: 0,
|
||||
slopeM: 0,
|
||||
};
|
||||
|
||||
/** 관 끝단을 자를 마감면 — 구조물(기슭막이·집수정)의 계류측 변을 **그대로 복사**한다. */
|
||||
export interface EndFace {
|
||||
base: OffsetPoint;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import { computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { CulvertLayout, InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { BasinAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { materialLabel, materialLimit } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
@@ -33,7 +34,10 @@ export interface RevetOffsetControl {
|
||||
/** 유입측 구조물 형식 선택(드롭다운 — 2026-08-22 사용자). 세션 보관은 Page가 한다. */
|
||||
export interface InletStructureControl {
|
||||
valueFor: (section: CrossSection) => InletStructureChoice;
|
||||
adjustFor: (section: CrossSection) => BasinAdjust;
|
||||
set: (chainageM: number, value: InletStructureChoice) => void;
|
||||
updateAdjust: (chainageM: number, patch: Partial<BasinAdjust>) => void;
|
||||
resetAdjust: (chainageM: number) => void;
|
||||
}
|
||||
|
||||
/** 유출측 다단 기슭막이 단 수 제어(2026-08-22 사용자 — 유출 벽 기준 숫자 입력). */
|
||||
@@ -77,6 +81,7 @@ export function computeCardCulvert(
|
||||
sourceSamples,
|
||||
adjustsInput(section, revetOffset, extraWalls),
|
||||
inletStructure?.valueFor(section),
|
||||
inletStructure?.adjustFor(section),
|
||||
equalizeExtras,
|
||||
);
|
||||
if (!layout) return null;
|
||||
|
||||
@@ -543,8 +543,22 @@ export function appendCrossDesignOverlay(
|
||||
| [{ offset_m: number; elevation_m: number }, { offset_m: number; elevation_m: number }]
|
||||
| null => {
|
||||
// 배수관 세트 트림 + 사면 끝 접점 트림을 합친 유효 범위.
|
||||
const minOffset = Math.max(trim?.minOffset ?? -Infinity, meetLeft ?? -Infinity);
|
||||
const maxOffset = Math.min(trim?.maxOffset ?? Infinity, meetRight ?? Infinity);
|
||||
// 배수관 레이아웃이 사면 폴리라인(min/maxSlope)을 준 쪽은 **그 시작점(노견)에서**
|
||||
// 설계선을 끊는다 — 그 바깥은 폴리라인이 그리므로 이중선이 되고, 트림 경계에서
|
||||
// 표고를 벽 상단으로 스냅하면서 수직으로 튀는 선이 남는다(2026-08-22 사용자 지적:
|
||||
// 집수정을 좌우로 옮기면 벽 상단이 노견 표고 그대로라 스냅 폭이 그만큼 커진다).
|
||||
const minSlopeStart = trim?.minSlope?.points[0]?.offset;
|
||||
const maxSlopeStart = trim?.maxSlope?.points[0]?.offset;
|
||||
const minOffset = Math.max(
|
||||
trim?.minOffset ?? -Infinity,
|
||||
meetLeft ?? -Infinity,
|
||||
minSlopeStart ?? -Infinity,
|
||||
);
|
||||
const maxOffset = Math.min(
|
||||
trim?.maxOffset ?? Infinity,
|
||||
meetRight ?? Infinity,
|
||||
maxSlopeStart ?? Infinity,
|
||||
);
|
||||
if (!Number.isFinite(minOffset) && !Number.isFinite(maxOffset)) return [a, b];
|
||||
const effective = { minOffset, maxOffset };
|
||||
const lo = Math.min(a.offset_m, b.offset_m);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { materialLimit, REVET_MATERIALS } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { BasinAdjust, WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { L } from "./B06_Section_UI_Section_Common";
|
||||
|
||||
/** 조정창이 쓰는 값·동작. 카드가 자기 상태에 맞춰 물려 준다. */
|
||||
@@ -40,6 +40,9 @@ export interface StructurePanelDeps {
|
||||
structureFor: () => InletStructureChoice;
|
||||
/** 유입측 구조물 형식 변경 — 카드를 다시 그린다. */
|
||||
setStructure: (value: InletStructureChoice) => void;
|
||||
basinAdjustFor: () => BasinAdjust;
|
||||
updateBasin: (patch: Partial<BasinAdjust>) => void;
|
||||
resetBasin: () => void;
|
||||
/** 이동 조작 가능 여부 — 집수정(자리 고정)은 숨긴다. */
|
||||
canNudge: (key: RevetKey) => boolean;
|
||||
/** 상황에 안 맞는 선택지 숨김(2026-08-22 사용자) — 기하가 판정한 가용성. */
|
||||
@@ -64,6 +67,7 @@ export interface StructurePanelHandle {
|
||||
*/
|
||||
const STEP_M = 1.0;
|
||||
const HEIGHT_STEP_M = 0.1;
|
||||
const BASIN_STEP_M = 0.1;
|
||||
|
||||
function makeButton(label: string, title: string, onClick: () => void): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
@@ -227,6 +231,45 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
});
|
||||
structureParts.controls.append(select);
|
||||
|
||||
const basinRow = makeRow("집수정 크기·위치");
|
||||
basinRow.controls.classList.add("b06-structure-panel__basin-buttons");
|
||||
const basinButton = (label: string, titleText: string, patch: () => Partial<BasinAdjust>) =>
|
||||
makeButton(label, titleText, () => deps.updateBasin(patch()));
|
||||
const basinHeightUp = basinButton("높이+", "내부 높이 +0.1m", () => ({
|
||||
innerHeightM: Math.min(2, deps.basinAdjustFor().innerHeightM + BASIN_STEP_M),
|
||||
}));
|
||||
const basinHeightDown = basinButton("높이-", "내부 높이 -0.1m", () => ({
|
||||
innerHeightM: Math.max(1, deps.basinAdjustFor().innerHeightM - BASIN_STEP_M),
|
||||
}));
|
||||
const basinWidthDown = basinButton("폭-", "내부 폭 -0.1m", () => ({
|
||||
innerWidthM: Math.max(1, deps.basinAdjustFor().innerWidthM - BASIN_STEP_M),
|
||||
}));
|
||||
const basinWidthUp = basinButton("폭+", "내부 폭 +0.1m", () => ({
|
||||
innerWidthM: Math.min(2, deps.basinAdjustFor().innerWidthM + BASIN_STEP_M),
|
||||
}));
|
||||
const basinSlopeUp = basinButton("사면▲", "1:1.2 성토선을 따라 위로", () => ({
|
||||
slopeM: Math.max(0, deps.basinAdjustFor().slopeM - BASIN_STEP_M),
|
||||
}));
|
||||
const basinSlopeDown = basinButton("사면▼", "1:1.2 성토선을 따라 아래로", () => ({
|
||||
slopeM: deps.basinAdjustFor().slopeM + BASIN_STEP_M,
|
||||
}));
|
||||
const basinIn = basinButton("노견◀", "노견측으로 0.1m", () => ({
|
||||
lateralM: Math.max(0, deps.basinAdjustFor().lateralM - BASIN_STEP_M),
|
||||
}));
|
||||
const basinOut = basinButton("바깥▶", "노견 반대측으로 0.1m", () => ({
|
||||
lateralM: deps.basinAdjustFor().lateralM + BASIN_STEP_M,
|
||||
}));
|
||||
basinRow.controls.append(
|
||||
basinHeightUp,
|
||||
basinHeightDown,
|
||||
basinWidthDown,
|
||||
basinWidthUp,
|
||||
basinSlopeUp,
|
||||
basinSlopeDown,
|
||||
basinIn,
|
||||
basinOut,
|
||||
);
|
||||
|
||||
// 다단 기슭막이 단 수(2026-08-22 사용자) — 높이 행과 같은 형식(- 값 +),
|
||||
// 직접 입력 없음. 현재 단 수는 show()가 채운다.
|
||||
const extraParts = makeRow(L("B06_Cross_Extra_Count"));
|
||||
@@ -248,8 +291,19 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
|
||||
const head = document.createElement("div");
|
||||
head.className = "b06-structure-panel__head";
|
||||
head.append(title, closeButton);
|
||||
root.append(head, value, structureRow, materialRow, heightRow, extraRow, moveRow.row);
|
||||
const basinReset = makeButton("↺", "집수정 기본값", () => deps.resetBasin());
|
||||
basinReset.classList.add("b06-structure-panel__basin-reset");
|
||||
head.append(title, basinReset, closeButton);
|
||||
root.append(
|
||||
head,
|
||||
value,
|
||||
structureRow,
|
||||
basinRow.row,
|
||||
materialRow,
|
||||
heightRow,
|
||||
extraRow,
|
||||
moveRow.row,
|
||||
);
|
||||
|
||||
return {
|
||||
root,
|
||||
@@ -259,6 +313,9 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
if (!key) return;
|
||||
const isExtra = key.startsWith("extra");
|
||||
const movable = deps.canNudge(key);
|
||||
const structure = deps.structureFor();
|
||||
const isBasin =
|
||||
key === "inlet" && (structure === "I" || structure === "L" || structure === "U");
|
||||
title.textContent = isExtra
|
||||
? `${L("B06_Cross_Revet_Extra")} ${Number(key.slice(5)) + 1}`
|
||||
: key === "outlet"
|
||||
@@ -269,7 +326,6 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
// 형식 선택은 유입측에서만, 이동·높이 조작은 기슭막이(집수정 제외)만.
|
||||
structureRow.classList.toggle("is-hidden", key !== "inlet");
|
||||
if (key === "inlet") {
|
||||
const structure = deps.structureFor();
|
||||
// 상황에 안 맞는 선택지는 숨긴다(2026-08-22 사용자). 단 지금 선택된 값은
|
||||
// 남긴다 — 숨기면 셀렉트가 빈 값이 된다.
|
||||
const allow = deps.optionsFor();
|
||||
@@ -283,6 +339,12 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
select.value = structure;
|
||||
}
|
||||
moveRow.row.classList.toggle("is-hidden", !movable);
|
||||
basinRow.row.classList.toggle("is-hidden", !isBasin);
|
||||
basinReset.classList.toggle("is-hidden", !isBasin);
|
||||
const isI = structure === "I";
|
||||
for (const button of [basinHeightUp, basinHeightDown, basinWidthDown, basinWidthUp]) {
|
||||
button.classList.toggle("is-hidden", isI);
|
||||
}
|
||||
materialRow.classList.toggle("is-hidden", !movable);
|
||||
heightRow.classList.toggle("is-hidden", !movable);
|
||||
if (movable) {
|
||||
|
||||
@@ -550,6 +550,19 @@ export function createCrossSectionCard(
|
||||
},
|
||||
structureFor: () => inletStructure?.valueFor(section) ?? "auto",
|
||||
setStructure: (value) => inletStructure?.set(section.chainage_m, value),
|
||||
basinAdjustFor: () =>
|
||||
inletStructure?.adjustFor(section) ?? {
|
||||
innerWidthM: 1,
|
||||
innerHeightM: 1.2,
|
||||
lateralM: 0,
|
||||
slopeM: 0,
|
||||
},
|
||||
updateBasin: (patch) =>
|
||||
inletStructure?.updateAdjust(section.chainage_m, {
|
||||
...inletStructure.adjustFor(section),
|
||||
...patch,
|
||||
}),
|
||||
resetBasin: () => inletStructure?.resetAdjust(section.chainage_m),
|
||||
// 집수정은 자리 고정 — 유입이 집수정이면 ◀/▶/↺를 숨긴다(추가 벽은 항상 이동).
|
||||
canNudge: (key) => key !== "inlet" || !culvertInletIsBasin,
|
||||
optionsFor: () => culvertInletOptions,
|
||||
|
||||
@@ -151,7 +151,11 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
standard_cross_section: standardPanel?.getValues(),
|
||||
});
|
||||
if (designRequestSeq.get(chainageM) !== seq) return;
|
||||
target.design = response.design;
|
||||
target.design = {
|
||||
...response.design,
|
||||
inlet_structure: target.design?.inlet_structure,
|
||||
basin_adjust: target.design?.basin_adjust,
|
||||
};
|
||||
sectionView.refreshCard(chainageM);
|
||||
} catch (error) {
|
||||
if (designRequestSeq.get(chainageM) !== seq) return;
|
||||
@@ -236,7 +240,11 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
const next = designByChainage.get(section.chainage_m.toFixed(3));
|
||||
if (next) {
|
||||
// full_designs 응답은 설계 전체(설계선 좌표 포함)라 통째로 교체한다.
|
||||
section.design = next as typeof section.design;
|
||||
section.design = {
|
||||
...(next as NonNullable<typeof section.design>),
|
||||
inlet_structure: section.design?.inlet_structure,
|
||||
basin_adjust: section.design?.basin_adjust,
|
||||
};
|
||||
sectionView.refreshCard(section.chainage_m);
|
||||
}
|
||||
}
|
||||
@@ -387,6 +395,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
const stationWidthControl = stationControls.stationWidth;
|
||||
const revetOffsetControl = stationControls.revetOffset;
|
||||
const stationWidths = stationControls.widths;
|
||||
const inletStructures = stationControls.inletStructures;
|
||||
const basinAdjustments = stationControls.basinAdjustments;
|
||||
|
||||
const sectionView = createSectionView(
|
||||
(chainageM, change) => {
|
||||
@@ -491,6 +501,12 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
stationWidths.forEach((width, chainage) => {
|
||||
patchFor(Number(chainage)).display_half_width_m = width;
|
||||
});
|
||||
inletStructures.forEach((structure, chainage) => {
|
||||
patchFor(Number(chainage)).inlet_structure = structure;
|
||||
});
|
||||
basinAdjustments.forEach((adjust, chainage) => {
|
||||
patchFor(Number(chainage)).basin_adjust = adjust;
|
||||
});
|
||||
const crossPatches: CrossSectionPatch[] = [...patchByChainage.values()];
|
||||
// 유토곡선은 화면 표시 내내 프론트 메모리에만 있다가 저장 시점에만 영구 저장된다.
|
||||
const result =
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { BasinAdjust, WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type {
|
||||
ExtraWallControl,
|
||||
InletStructureControl,
|
||||
@@ -18,7 +18,9 @@ import type {
|
||||
|
||||
/** 제어가 페이지에서 가져다 쓰는 값들 — 클로저 대신 함수로 받아 결합을 끊는다. */
|
||||
export interface StationControlDeps {
|
||||
sessionKey: (kind: "crossw" | "revetx" | "inletstruct" | "extrawall") => string | null;
|
||||
sessionKey: (
|
||||
kind: "crossw" | "revetx" | "inletstruct" | "basinadjust" | "extrawall",
|
||||
) => string | null;
|
||||
refreshCard: (chainageM: number) => void;
|
||||
detail: () => SectionDetailResponse | null;
|
||||
crossHalfWidth: () => number | undefined;
|
||||
@@ -32,6 +34,8 @@ export interface StationControls {
|
||||
inletStructure: InletStructureControl;
|
||||
extraWalls: ExtraWallControl;
|
||||
widths: Map<string, number>;
|
||||
inletStructures: Map<string, InletStructureChoice>;
|
||||
basinAdjustments: Map<string, BasinAdjust>;
|
||||
load: () => void;
|
||||
/** 전체 반영 — 개별 반폭을 전역값으로 덮는다(없으면 비운다). */
|
||||
applyGlobalWidth: (requested: number | undefined, chainages: number[]) => void;
|
||||
@@ -201,7 +205,32 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
/* ── 유입측 구조물 형식(2026-08-22 사용자 — 드롭다운) ────────────────
|
||||
* auto(규칙)/revet(기슭막이+배관)/I/L/U(집수정 형식). 세션에만 담는다. */
|
||||
const inletStructures = new Map<string, InletStructureChoice>();
|
||||
const basinAdjustments = new Map<string, BasinAdjust>();
|
||||
const structSessionKey = (): string | null => deps.sessionKey("inletstruct");
|
||||
const basinSessionKey = (): string | null => deps.sessionKey("basinadjust");
|
||||
|
||||
function loadBasinAdjustments(): void {
|
||||
basinAdjustments.clear();
|
||||
const key = basinSessionKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
const parsed = JSON.parse(window.sessionStorage.getItem(key) ?? "{}") as Record<
|
||||
string,
|
||||
BasinAdjust
|
||||
>;
|
||||
Object.entries(parsed).forEach(([chainage, value]) =>
|
||||
basinAdjustments.set(chainage, { ...DEFAULT_BASIN_ADJUST, ...value }),
|
||||
);
|
||||
} catch {
|
||||
/* 손상된 세션 값은 기본값으로 대체. */
|
||||
}
|
||||
}
|
||||
|
||||
function persistBasinAdjustments(): void {
|
||||
const key = basinSessionKey();
|
||||
if (key)
|
||||
window.sessionStorage.setItem(key, JSON.stringify(Object.fromEntries(basinAdjustments)));
|
||||
}
|
||||
|
||||
function loadInletStructures(): void {
|
||||
inletStructures.clear();
|
||||
@@ -300,13 +329,38 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
};
|
||||
|
||||
const inletStructureControl: InletStructureControl = {
|
||||
valueFor: (section) => inletStructures.get(section.chainage_m.toFixed(2)) ?? "auto",
|
||||
valueFor: (section) =>
|
||||
inletStructures.get(section.chainage_m.toFixed(2)) ??
|
||||
section.design?.inlet_structure ??
|
||||
"auto",
|
||||
adjustFor: (section) =>
|
||||
basinAdjustments.get(section.chainage_m.toFixed(2)) ?? {
|
||||
...DEFAULT_BASIN_ADJUST,
|
||||
...(section.design?.basin_adjust ?? {}),
|
||||
},
|
||||
set: (chainageM, value) => {
|
||||
if (value === "auto") inletStructures.delete(chainageM.toFixed(2));
|
||||
else inletStructures.set(chainageM.toFixed(2), value);
|
||||
inletStructures.set(chainageM.toFixed(2), value);
|
||||
persistInletStructures();
|
||||
deps.refreshCard(chainageM);
|
||||
},
|
||||
updateAdjust: (chainageM, patch) => {
|
||||
const key = chainageM.toFixed(2);
|
||||
const current = basinAdjustments.get(key) ?? { ...DEFAULT_BASIN_ADJUST };
|
||||
const next = { ...current, ...patch };
|
||||
basinAdjustments.set(key, {
|
||||
innerWidthM: Math.min(2, Math.max(1, round1(next.innerWidthM))),
|
||||
innerHeightM: Math.min(2, Math.max(1, round1(next.innerHeightM))),
|
||||
lateralM: Math.max(0, clampMove(next.lateralM)),
|
||||
slopeM: Math.max(0, clampMove(next.slopeM)),
|
||||
});
|
||||
persistBasinAdjustments();
|
||||
deps.refreshCard(chainageM);
|
||||
},
|
||||
resetAdjust: (chainageM) => {
|
||||
basinAdjustments.set(chainageM.toFixed(2), { ...DEFAULT_BASIN_ADJUST });
|
||||
persistBasinAdjustments();
|
||||
deps.refreshCard(chainageM);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -315,10 +369,13 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
inletStructure: inletStructureControl,
|
||||
extraWalls: extraWallControl,
|
||||
widths: stationWidths,
|
||||
inletStructures,
|
||||
basinAdjustments,
|
||||
load: () => {
|
||||
loadStationWidths();
|
||||
loadRevetShifts();
|
||||
loadInletStructures();
|
||||
loadBasinAdjustments();
|
||||
loadExtraCounts();
|
||||
},
|
||||
applyGlobalWidth: (requested, chainages) => {
|
||||
|
||||
@@ -107,6 +107,16 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.b06-structure-panel__basin-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, max-content);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.b06-structure-panel__basin-reset {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.b06-structure-panel__select {
|
||||
flex: 1;
|
||||
padding: 1px var(--spacing-4);
|
||||
|
||||
Reference in New Issue
Block a user