feat(B06): 집수정 관 끝선·두께 0.2·구조물 형식 드롭다운
2026-08-22 사용자 요청 3건: - 관 끝선 = I형 벽의 안쪽(물받는 쪽) 변 복사(기슭막이와 동일 방식). 관 시작점(하단): ㄴ·ㄷ형은 측벽 안쪽 변×바닥 윗면 교차점, I형은 내공 1.0m 유지. - 집수정 부재(벽·바닥판) 두께 0.2m(BASIN_MEMBER_THICKNESS_M). - 유입 구조물 형식 드롭다운: 자동(규칙)/기슭막이+배관/집수정 I·ㄴ·ㄷ형. 집수정 클릭 선택 가능, 선택 시 조정창에 드롭다운(집수정은 ◀▶ 숨김). 세션 보관(inletstruct)·revet 선택 시 양측성토 로직 복귀·manual 사유. - 집수정 구성을 _Cross_Culvert_Basin.ts로 분리(700줄 제한 — Geom 690줄). I형 바닥판 제거(벽뿐 — 정의 정합). - 검증: 13+15.7 모듈(auto/revet/I/L/U 전부 기대 형상·관유입 자리 일치) + UI 실조작(집수정 클릭→드롭다운 revet/U/auto 전환, 부재 수 2→0→3→2, 세션 왕복) + 스크린샷 대조. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,7 +200,9 @@ export function appendCulvertOverlay(
|
||||
const reasonText =
|
||||
basin.reason === "cut"
|
||||
? "절토측 유입 — 집수정 기본(성토사면 없음)"
|
||||
: "유입측 성토사면 3m 이하 — 기슭막이 대신 집수정 기본(선택 기능 추가 예정)";
|
||||
: basin.reason === "manual"
|
||||
? "사용자 선택 집수정(규칙상 기슭막이 자리)"
|
||||
: "유입측 성토사면 3m 이하 — 기슭막이 대신 집수정 기본";
|
||||
for (const part of basin.parts) {
|
||||
layer.append(
|
||||
polygon(
|
||||
@@ -310,6 +312,22 @@ export function appendCulvertOverlay(
|
||||
});
|
||||
layer.append(hit);
|
||||
}
|
||||
// 집수정도 선택 대상(2026-08-22 사용자 — 형식 드롭다운). 자리 이동은 없고
|
||||
// 조정창에서 구조물 형식만 고른다.
|
||||
if (layout.basin) {
|
||||
for (const part of layout.basin.parts) {
|
||||
const hit = polygon(
|
||||
part.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]),
|
||||
"b06-chart__culvert-revet-hit",
|
||||
"유입 집수정 선택 — 조정창에서 구조물 형식 선택",
|
||||
);
|
||||
hit.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
onSelectRevet("inlet");
|
||||
});
|
||||
layer.append(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 강조는 클래스만 갈아 끼운다 — 카드를 다시 그리면 휠 줌·팬이 초기화된다.
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_Culvert_Basin.ts
|
||||
* 유입 **집수정** 단면 구성 — 기하 본체(`_Cross_Culvert_Geom.ts`)에서 분리(700줄 제한).
|
||||
*
|
||||
* 집수정은 기슭막이와 달리 **물을 모으려고 만든 구조물**이다(2026-08-22 사용자 확정):
|
||||
* · I형(공유벽뿐): 배관은 원지반 자리 유지. 기본 높이(관경+여유)로 노견 끝 접점이
|
||||
* 안 되면 벽을 노견 표고까지 키운다.
|
||||
* · ㄴ(L)형(= I형 + 바닥판, 기본값): 내부 높이 1.2m(임시값) 등 사이즈를 유지한 채
|
||||
* 통째로 노견 끝점에 일치한다 — 바닥(=관 유입 invert)이 따라 올라 관 물매가 바뀐다.
|
||||
* · ㄷ(U)형: ㄴ형 + 반대측(도로측) 막음벽(I형 수직반전).
|
||||
* 부재(벽·바닥판) 두께 0.2m(2026-08-22 사용자 지정). 벽 자리 = 상단 도로측 꼭짓점이
|
||||
* **노견 끝 지점**에 닿는 자리(우측 집수정이면 상단 좌측점).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
BASIN_INNER_HEIGHT_M,
|
||||
BASIN_MEMBER_THICKNESS_M,
|
||||
REVET_FREEBOARD_M,
|
||||
REVET_LEAN_RATIO,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type {
|
||||
BasinLayout,
|
||||
BasinShape,
|
||||
EndFace,
|
||||
OffsetPoint,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
/** 집수정 구성 입력 — 기하 본체가 자기 상태를 명시적으로 물려 준다. */
|
||||
export interface BasinBuildInput {
|
||||
/** 관 유입 기준점(노견 끝 offset, invert 표고 — ㄴ·ㄷ형은 상승분 반영 후). */
|
||||
anchor: OffsetPoint;
|
||||
/** 계류측 방향 부호(+ = 좌). */
|
||||
outward: number;
|
||||
shape: BasinShape;
|
||||
reason: BasinLayout["reason"];
|
||||
/** 관 외경 지름(m). */
|
||||
diameterM: number;
|
||||
/** 이 측의 노견 끝 점 — 벽 상단 접점·I형 높이 성장 기준. */
|
||||
edge: { offset_m: number; elevation_m: number };
|
||||
groundAt: (offset: number) => number;
|
||||
/** 표준단면 절토 경사(1:n) — 절토 계획선용. */
|
||||
cutSlopeRatio: number;
|
||||
}
|
||||
|
||||
/** 집수정 구성 결과 — 본체가 트림·관 끝단면·관 시작점에 나눠 꽂는다. */
|
||||
export interface BasinBuildResult {
|
||||
basin: BasinLayout;
|
||||
/** 관 끝단 마감면 = I형 벽의 안쪽(물받는 쪽) 변 복사(2026-08-22 사용자 확정). */
|
||||
endFace: EndFace;
|
||||
/** 관 시작점(하단 기준) — ㄴ·ㄷ형은 측벽 안쪽 변×바닥 윗면 교차점, I형은 내공 1.0m. */
|
||||
pipeEnd: OffsetPoint;
|
||||
/** 성토 사면선 트림 경계(벽 상단 도로측 꼭짓점)와 그 표고. */
|
||||
trimOffset: number;
|
||||
trimElevation: number;
|
||||
}
|
||||
|
||||
/** 실무 내공 폭(m) — 울진 돌집수정. */
|
||||
const INNER_WIDTH_M = 1.0;
|
||||
|
||||
/** 집수정 단면을 만든다. 형태별 규칙은 파일 머리말 참조. */
|
||||
export function buildBasin(input: BasinBuildInput): BasinBuildResult {
|
||||
const { anchor, outward, shape, reason, diameterM, edge, groundAt, cutSlopeRatio } = input;
|
||||
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;
|
||||
// 기움은 **수직 기준 반전** — 상단이 도로측(내공 쪽)으로 1:0.3 물러난다(2026-08-20).
|
||||
const wallOf = (baseOffset: number, dir: number): OffsetPoint[] => {
|
||||
const topShift = -dir * REVET_LEAN_RATIO * wallHeight;
|
||||
return [
|
||||
{ offset: baseOffset, elevation: anchor.elevation - floorThickness },
|
||||
{ offset: baseOffset + topShift, elevation: anchor.elevation + wallHeight },
|
||||
{
|
||||
offset: baseOffset + topShift + dir * memberT,
|
||||
elevation: anchor.elevation + wallHeight,
|
||||
},
|
||||
{
|
||||
offset: baseOffset + dir * memberT,
|
||||
elevation: anchor.elevation - floorThickness,
|
||||
},
|
||||
];
|
||||
};
|
||||
// I형 벽 자리 = **상단 도로측 꼭짓점이 노견 끝 지점에 닿는다**(2026-08-22 사용자
|
||||
// 확정). 벽 상단이 도로측으로 1:0.3 물러나므로 하단(base)은 그만큼 계류측 바깥.
|
||||
const wallBase = anchor.offset + outward * (REVET_LEAN_RATIO * wallHeight);
|
||||
const parts: BasinLayout["parts"] = [{ kind: "wall", points: wallOf(wallBase, outward) }];
|
||||
// ㄴ형 = I형 + 바닥판. 바닥은 벽 바깥(계류측) 변에 맞대고 그 너머로 뻗는다
|
||||
// (2026-08-20 확정). 벽이 기울어 있어 안쪽 변도 벽 바깥면 선을 그대로 따른다.
|
||||
const wallOuterAt = (elevation: number): number => {
|
||||
const bottom = anchor.elevation - floorThickness;
|
||||
const t = (elevation - bottom) / (wallHeight + floorThickness);
|
||||
return wallBase + outward * memberT - outward * REVET_LEAN_RATIO * wallHeight * t;
|
||||
};
|
||||
const floorTopInner = wallOuterAt(anchor.elevation);
|
||||
const floorBottomInner = wallOuterAt(anchor.elevation - floorThickness);
|
||||
const floorOuter = floorTopInner + outward * (INNER_WIDTH_M + memberT);
|
||||
const floorTaper = REVET_LEAN_RATIO * floorThickness;
|
||||
if (shape !== "I") {
|
||||
parts.push({
|
||||
kind: "floor",
|
||||
points: [
|
||||
{ offset: floorTopInner, elevation: anchor.elevation },
|
||||
{ offset: floorOuter, elevation: anchor.elevation },
|
||||
{
|
||||
offset: floorOuter - outward * floorTaper,
|
||||
elevation: anchor.elevation - floorThickness,
|
||||
},
|
||||
{ offset: floorBottomInner, elevation: anchor.elevation - floorThickness },
|
||||
],
|
||||
});
|
||||
}
|
||||
// ㄷ형 = ㄴ형 + 반대측(도로측) 막음벽(I형 수직반전 — 2026-08-22 사용자 확정).
|
||||
if (shape === "U") {
|
||||
parts.push({ kind: "wall", points: wallOf(anchor.offset, -outward) });
|
||||
}
|
||||
// ㄴ·ㄷ형이 원지반 **안쪽**에 박히면 그만큼 절토가 필요하다(2026-08-20 사용자 ①).
|
||||
let basinCut: BasinLayout["cutLine"] = null;
|
||||
if (shape !== "I") {
|
||||
const outerTop = { offset: floorOuter, elevation: anchor.elevation };
|
||||
if (groundAt(floorOuter) > anchor.elevation + 0.05) {
|
||||
for (let h = 0.05; h <= 20; h += 0.05) {
|
||||
const probe = floorOuter + outward * cutSlopeRatio * h;
|
||||
if (groundAt(probe) <= anchor.elevation + h) {
|
||||
basinCut = { from: outerTop, to: { offset: probe, elevation: anchor.elevation + h } };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
basin: {
|
||||
shape,
|
||||
parts,
|
||||
// 라벨 자리 = I형 벽 하단(2026-08-20 사용자) — 그리기 쪽에서 아래로 내려 붙인다.
|
||||
label: {
|
||||
offset: wallBase + outward * (memberT / 2),
|
||||
elevation: anchor.elevation - floorThickness,
|
||||
},
|
||||
reason,
|
||||
cutLine: basinCut,
|
||||
},
|
||||
// 관 끝선 = I형 벽의 안쪽(물받는 쪽 = 내공측) 변 복사(2026-08-22 사용자 확정).
|
||||
endFace: {
|
||||
base: {
|
||||
offset: wallBase + outward * memberT,
|
||||
elevation: anchor.elevation - floorThickness,
|
||||
},
|
||||
// 변 전체(바닥−두께 ~ 상단) 기울기 — x 이동량은 벽 기움(0.3×벽높이).
|
||||
direction: {
|
||||
offset: -outward * REVET_LEAN_RATIO * wallHeight,
|
||||
elevation: wallHeight + floorThickness,
|
||||
},
|
||||
},
|
||||
pipeEnd:
|
||||
shape === "I"
|
||||
? { offset: anchor.offset + outward * INNER_WIDTH_M, elevation: anchor.elevation }
|
||||
: { offset: floorTopInner, elevation: anchor.elevation },
|
||||
trimOffset: wallBase - outward * REVET_LEAN_RATIO * wallHeight,
|
||||
trimElevation: anchor.elevation + wallHeight,
|
||||
};
|
||||
}
|
||||
@@ -135,3 +135,6 @@ export const BASIN_INNER_HEIGHT_M = 1.2;
|
||||
* 성토사면이 없으므로 자동 포함. 사용자 선택(기슭막이 유지)은 향후 추가 예정.
|
||||
*/
|
||||
export const BASIN_MAX_FILL_SLOPE_M = 3.0;
|
||||
|
||||
/** 집수정 부재(벽·바닥판) 두께(m) — 2026-08-22 사용자 지정 0.2. */
|
||||
export const BASIN_MEMBER_THICKNESS_M = 0.2;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
BASIN_BOTTOM_CLEARANCE_M,
|
||||
BASIN_INNER_HEIGHT_M,
|
||||
BASIN_MAX_FILL_SLOPE_M,
|
||||
FILL_MIN_RISE_M,
|
||||
@@ -27,7 +26,6 @@ import {
|
||||
MIN_PIPE_COVER_M,
|
||||
PITCHING_THICKNESS_M,
|
||||
REVET_EMBED_DEPTH_M,
|
||||
REVET_FREEBOARD_M,
|
||||
REVET_LEAN_RATIO,
|
||||
REVET_THICKNESS_M,
|
||||
revetHeightLimit,
|
||||
@@ -47,8 +45,10 @@ import type {
|
||||
export * from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
export * from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
import { buildBasin } from "./B06_Section_UI_Cross_Culvert_Basin";
|
||||
import type { FillSlopeSegment, WallVertical } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import {
|
||||
clampToFace,
|
||||
clampWallOffset,
|
||||
cutLength,
|
||||
minShoulderWallOffset,
|
||||
@@ -58,16 +58,22 @@ import {
|
||||
designInterpolator,
|
||||
intersect,
|
||||
slopeToeOffset,
|
||||
slopeLengthAlong,
|
||||
solveWallVertical,
|
||||
STRAY_LIMIT_M,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
|
||||
/** 유입측 구조물 사용자 선택(2026-08-22) — auto = 규칙(사면≤3m → 집수정 ㄴ형). */
|
||||
export type InletStructureChoice = "auto" | "revet" | "I" | "L" | "U";
|
||||
|
||||
/** 배수관 세트 기하 계산. 부족한 입력이면 null — 그리기와 분리(설계선 트림이 먼저 쓴다). */
|
||||
export function computeCulvertLayout(
|
||||
section: CrossSection,
|
||||
groundSamples: SectionSample[],
|
||||
/** 사용자가 손으로 민 기슭막이 X 이동량(m, + = 계류측 바깥). 없으면 자동 자리. */
|
||||
revetShift?: { inlet?: number; outlet?: number },
|
||||
/** 유입측 구조물 형식 선택(드롭다운) — 없으면 auto(규칙). */
|
||||
inletStructure?: InletStructureChoice,
|
||||
): CulvertLayout | null {
|
||||
const culvert = section.culvert;
|
||||
if (!culvert) return null;
|
||||
@@ -108,26 +114,32 @@ export function computeCulvertLayout(
|
||||
mode === "both_cut" ||
|
||||
(mode === "left_cut" && inletSideName === "left") ||
|
||||
(mode === "right_cut" && inletSideName === "right");
|
||||
let inletFillSlopeLen = 0;
|
||||
if (!inletIsCut && designAt) {
|
||||
const toe = slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit);
|
||||
const from = inletInfo.edge.offset_m;
|
||||
const steps = 60;
|
||||
let prevElev = designAt(from);
|
||||
for (let i = 1; i <= steps; i += 1) {
|
||||
const o = from + ((toe - from) * i) / steps;
|
||||
const e = designAt(o);
|
||||
inletFillSlopeLen += Math.hypot((toe - from) / steps, e - prevElev);
|
||||
prevElev = e;
|
||||
}
|
||||
}
|
||||
const basinReason: BasinLayout["reason"] | null = inletIsCut
|
||||
const inletFillSlopeLen =
|
||||
!inletIsCut && designAt
|
||||
? slopeLengthAlong(
|
||||
designAt,
|
||||
inletInfo.edge.offset_m,
|
||||
slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit),
|
||||
)
|
||||
: 0;
|
||||
const ruleReason: BasinLayout["reason"] | null = inletIsCut
|
||||
? "cut"
|
||||
: inletFillSlopeLen <= BASIN_MAX_FILL_SLOPE_M + 1e-6
|
||||
? "short"
|
||||
: null;
|
||||
// 집수정 형식 기본값 = ㄴ(L)형(2026-08-20 확정). 사용자 선택은 향후 추가 예정.
|
||||
const basinShape: BasinShape = "L";
|
||||
: culvert.inlet.structure === "집수정"
|
||||
? "cut"
|
||||
: 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";
|
||||
// ㄴ·ㄷ형은 **측면·바닥 사이즈를 유지한 채 통째로 노견 끝점에 일치**시킨다(2026-08-22
|
||||
// 사용자 확정 — 노견과 빈 공간을 채운다). 바닥(=관 유입 invert)이 따라 올라가므로
|
||||
// 관 물매도 바뀐다. 유출단은 원지반 위 원칙 그대로. I형은 원지반 배관을 유지하고
|
||||
@@ -196,18 +208,9 @@ export function computeCulvertLayout(
|
||||
? slopeToeOffset(designAt, groundAt, outletInfo.edge.offset_m, outletInfo.limit)
|
||||
: outletInfo.edge.offset_m;
|
||||
// 성토사면 경사길이 실측(노견 → 사면 끝) — 법정 5m 이내 충족 확인용(사용자 ①).
|
||||
let fillSlopeLength = 0;
|
||||
if (designAt) {
|
||||
const from = outletInfo.edge.offset_m;
|
||||
const steps = 60;
|
||||
let prevElev = designAt(from);
|
||||
for (let i = 1; i <= steps; i += 1) {
|
||||
const o = from + ((outletAnchorOffset - from) * i) / steps;
|
||||
const e = designAt(o);
|
||||
fillSlopeLength += Math.hypot((outletAnchorOffset - from) / steps, e - prevElev);
|
||||
prevElev = e;
|
||||
}
|
||||
}
|
||||
const fillSlopeLength = designAt
|
||||
? slopeLengthAlong(designAt, outletInfo.edge.offset_m, outletAnchorOffset)
|
||||
: 0;
|
||||
// 역경사는 수평으로 클램프(수평 가능 — 사용자 확정).
|
||||
const outletInvert = Math.min(groundAt(outletAnchorOffset), inlet.elevation);
|
||||
const outletAnchor: OffsetPoint = { offset: outletAnchorOffset, elevation: outletInvert };
|
||||
@@ -257,135 +260,32 @@ export function computeCulvertLayout(
|
||||
vertical: WallVertical | null = null,
|
||||
): WallLayout | null => {
|
||||
const reason: BasinLayout["reason"] | null =
|
||||
forceBasinReason ?? (spec.structure === "집수정" ? "cut" : null);
|
||||
// 백엔드 spec의 "집수정"은 판정(ruleReason)에 이미 반영됐다 — 사용자 선택
|
||||
// revet(기슭막이 강제)를 존중하려면 여기서 다시 살리면 안 된다(2026-08-22).
|
||||
forceBasinReason;
|
||||
if (reason) {
|
||||
// 집수정 = 기슭막이와 같은 **평행사변형 벽**(+ 형식에 따라 바닥·반대측 막음).
|
||||
// 형식 기본값 = ㄴ(L)형(함수 상단 basinShape). 실무 내공 1.0m(울진 돌집수정).
|
||||
const shape: BasinShape = basinShape;
|
||||
const innerWidth = 1.0;
|
||||
// 집수정은 기슭막이와 달리 **물을 모으려고 만든 구조물**이다(2026-08-22 사용자
|
||||
// 확정) — 형태별로 별도 구성하고, 배관(invert)은 원지반 자리를 그대로 지킨다.
|
||||
// · I형(공유벽뿐): 원지반이 낮아 기본 높이(관경+여유)로 노견 끝 접점이 안
|
||||
// 되면 배관은 그대로 두고 **벽을 노견 표고까지 키운다**.
|
||||
// · ㄴ(L)·ㄷ형(바닥판이 원지반 대체): 벽을 키우지 않는다 — 내부 높이(바닥
|
||||
// 윗면~벽 상단)는 BASIN_INNER_HEIGHT_M(임시 1.2) 고정.
|
||||
const floorThickness = BASIN_BOTTOM_CLEARANCE_M;
|
||||
const roleEdge = spec.role === "inlet" ? inletInfo.edge : outletInfo.edge;
|
||||
const wallHeight =
|
||||
shape === "I"
|
||||
? Math.max(diameter + REVET_FREEBOARD_M, roleEdge.elevation_m - anchor.elevation)
|
||||
: BASIN_INNER_HEIGHT_M;
|
||||
// 벽 단면은 기슭막이와 동일(두께 0.45). 기움은 **수직 기준 반전** — 상단이
|
||||
// 도로측(내공 쪽)으로 1:0.3 물러난다(2026-08-20 사용자 정정).
|
||||
const wallOf = (baseOffset: number, dir: number): OffsetPoint[] => {
|
||||
const topShift = -dir * REVET_LEAN_RATIO * wallHeight;
|
||||
return [
|
||||
{ offset: baseOffset, elevation: anchor.elevation - floorThickness },
|
||||
{ offset: baseOffset + topShift, elevation: anchor.elevation + wallHeight },
|
||||
{
|
||||
offset: baseOffset + topShift + dir * REVET_THICKNESS_M,
|
||||
elevation: anchor.elevation + wallHeight,
|
||||
},
|
||||
{
|
||||
offset: baseOffset + dir * REVET_THICKNESS_M,
|
||||
elevation: anchor.elevation - floorThickness,
|
||||
},
|
||||
];
|
||||
};
|
||||
// I형 벽 자리 = **상단 도로측 꼭짓점이 노견 끝 지점에 닿는다**(2026-08-22 사용자
|
||||
// 확정 — 우측 집수정이면 상단 좌측점, 좌측이면 우측점이 접점). 벽 상단이
|
||||
// 도로측으로 1:0.3 물러나므로 하단(base)은 그만큼 계류측 바깥이다.
|
||||
// anchor.offset = 노견 끝(관 유입단 기준점), 상단 표고는 invert+관경+여유고라
|
||||
// 관이 토피 상한(노면−관경−토피)에 묻힌 표준 단면에서는 노견 표고와도 일치한다.
|
||||
const wallBase = anchor.offset + outward * (REVET_LEAN_RATIO * wallHeight);
|
||||
const parts: BasinLayout["parts"] = [{ kind: "wall", points: wallOf(wallBase, outward) }];
|
||||
// ㄴ형 = I형 + 바닥판. 바닥은 **아래가 짧은 사다리꼴**(윗변이 길고 아랫변이
|
||||
// 짧다 — 벽과 같은 1:0.3 물매가 양 끝에 붙는다). 위치는 **I형 벽 기준 반대측**
|
||||
// = 도로측으로 뻗는다(2026-08-20 사용자 확정).
|
||||
// 바닥은 **I형 벽의 바깥(계류측) 변에 좌측 변을 맞대고** 그 너머로 뻗는다
|
||||
// (2026-08-20 사용자 확정). 벽이 기울어 있으므로 바닥 좌측 변도 **벽 바깥면 선을
|
||||
// 그대로 따라** 기울여야 틈 없이 붙는다(높이마다 벽 x가 달라진다).
|
||||
const wallOuterAt = (elevation: number): number => {
|
||||
const bottom = anchor.elevation - floorThickness;
|
||||
const t = (elevation - bottom) / (wallHeight + floorThickness);
|
||||
return wallBase + outward * REVET_THICKNESS_M - outward * REVET_LEAN_RATIO * wallHeight * t;
|
||||
};
|
||||
const floorTopInner = wallOuterAt(anchor.elevation);
|
||||
const floorBottomInner = wallOuterAt(anchor.elevation - floorThickness);
|
||||
const floorOuter = floorTopInner + outward * (innerWidth + REVET_THICKNESS_M);
|
||||
const floorTaper = REVET_LEAN_RATIO * floorThickness;
|
||||
parts.push({
|
||||
kind: "floor",
|
||||
points: [
|
||||
{ offset: floorTopInner, elevation: anchor.elevation },
|
||||
{ offset: floorOuter, elevation: anchor.elevation },
|
||||
{
|
||||
offset: floorOuter - outward * floorTaper,
|
||||
elevation: anchor.elevation - floorThickness,
|
||||
},
|
||||
{ offset: floorBottomInner, elevation: anchor.elevation - floorThickness },
|
||||
],
|
||||
});
|
||||
// ㄷ형이면 반대측(도로측) 막음벽을 하나 더 세운다 — 지금은 L형 기본이라 미사용.
|
||||
if ((shape as BasinShape) === "U") {
|
||||
parts.push({ kind: "wall", points: wallOf(anchor.offset, -outward) });
|
||||
}
|
||||
// ㄴ·ㄷ형이 원지반 **안쪽**에 박히면 그만큼 절토가 필요하다(2026-08-20 사용자 ①).
|
||||
// 구조물 바깥 끝 상단에서 표준단면 절토경사(1:n)로 원지반과 만나는 점까지 긋는다.
|
||||
let basinCut: BasinLayout["cutLine"] = null;
|
||||
if ((shape as BasinShape) !== "I") {
|
||||
const outerTop = { offset: floorOuter, elevation: anchor.elevation };
|
||||
if (groundAt(floorOuter) > anchor.elevation + 0.05) {
|
||||
const cutRatio = section.design?.cut_slope_ratio ?? 1.0;
|
||||
for (let h = 0.05; h <= 20; h += 0.05) {
|
||||
const probe = floorOuter + outward * cutRatio * h;
|
||||
if (groundAt(probe) <= anchor.elevation + h) {
|
||||
basinCut = {
|
||||
from: outerTop,
|
||||
to: { offset: probe, elevation: anchor.elevation + h },
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
basin = {
|
||||
shape,
|
||||
parts,
|
||||
// 라벨 자리 = **I형 벽 하단**(2026-08-20 사용자). 상단에 두면 노면·계획선과
|
||||
// 겹친다 — 벽 바닥 변 중앙에 두고 그리기 쪽에서 아래로 내려 붙인다.
|
||||
label: {
|
||||
offset: wallBase + outward * (REVET_THICKNESS_M / 2),
|
||||
elevation: anchor.elevation - floorThickness,
|
||||
},
|
||||
// 집수정 구성은 분리 파일(_Cross_Culvert_Basin.ts — 700줄 제한)이 맡는다.
|
||||
const built = buildBasin({
|
||||
anchor,
|
||||
outward,
|
||||
shape: basinShape,
|
||||
reason,
|
||||
cutLine: basinCut,
|
||||
};
|
||||
// 성토 사면선은 **벽 상단 도로측 꼭지점(교점)**에서 끊는다 — 바닥 끝을 기준으로
|
||||
// 두면 사면선이 집수정을 뚫고 들어간다(2026-08-20 사용자 ①).
|
||||
const wallTopInner = wallBase - outward * REVET_LEAN_RATIO * wallHeight;
|
||||
diameterM: diameter,
|
||||
edge: spec.role === "inlet" ? inletInfo.edge : outletInfo.edge,
|
||||
groundAt,
|
||||
cutSlopeRatio: section.design?.cut_slope_ratio ?? 1.0,
|
||||
});
|
||||
basin = built.basin;
|
||||
basinPipeEnd = built.pipeEnd;
|
||||
endFaces[spec.role] = built.endFace;
|
||||
// 성토 사면선은 벽 상단 도로측 꼭짓점에서 끊고, 끝단 표고도 거기 맞춘다.
|
||||
if (outward > 0) {
|
||||
trimMax = wallTopInner;
|
||||
trimMaxElevation = anchor.elevation + wallHeight;
|
||||
trimMax = built.trimOffset;
|
||||
trimMaxElevation = built.trimElevation;
|
||||
} else {
|
||||
trimMin = wallTopInner;
|
||||
trimMinElevation = anchor.elevation + wallHeight;
|
||||
trimMin = built.trimOffset;
|
||||
trimMinElevation = built.trimElevation;
|
||||
}
|
||||
// 유입 배관 위치는 **현재 유지**(2026-08-22 사용자 확정) — 관 유입단은 종전처럼
|
||||
// 노견 끝에서 내공 1.0m 자리다. 벽이 노견에 붙으면서 관 끝이 벽을 지나 집수정
|
||||
// 안으로 들어가므로, 마감면은 벽 변을 복사하지 않고 **관 끝 자리에서 벽과 나란한
|
||||
// 기울기**로 자른다(벽 변에 자르면 관이 벽 앞에서 끊겨 위치가 바뀐다).
|
||||
basinPipeEnd = {
|
||||
offset: anchor.offset + outward * innerWidth,
|
||||
elevation: anchor.elevation,
|
||||
};
|
||||
endFaces[spec.role] = {
|
||||
base: { offset: basinPipeEnd.offset, elevation: anchor.elevation - floorThickness },
|
||||
direction: {
|
||||
offset: -outward * REVET_LEAN_RATIO * (wallHeight + floorThickness),
|
||||
elevation: wallHeight + floorThickness,
|
||||
},
|
||||
};
|
||||
return null;
|
||||
}
|
||||
// 형상(사용자 스케치 확정 — 좌측 벽 기준, 우측은 반전): 배면(도로측) 수직, 전면
|
||||
@@ -568,23 +468,6 @@ export function computeCulvertLayout(
|
||||
};
|
||||
};
|
||||
deriveAxis();
|
||||
/** 교점을 마감면 **선분 안**으로 자른다 — 선분 밖(벽 하단 아래 등)으로 새지 않게. */
|
||||
const clampToFace = (
|
||||
point: OffsetPoint,
|
||||
face: { base: OffsetPoint; direction: { offset: number; elevation: number } },
|
||||
): OffsetPoint => {
|
||||
const dot =
|
||||
(point.offset - face.base.offset) * face.direction.offset +
|
||||
(point.elevation - face.base.elevation) * face.direction.elevation;
|
||||
const len2 =
|
||||
face.direction.offset * face.direction.offset +
|
||||
face.direction.elevation * face.direction.elevation;
|
||||
const t = len2 > 1e-12 ? Math.min(Math.max(dot / len2, 0), 1) : 0;
|
||||
return {
|
||||
offset: face.base.offset + face.direction.offset * t,
|
||||
elevation: face.base.elevation + face.direction.elevation * t,
|
||||
};
|
||||
};
|
||||
const endCorners = (role: "inlet" | "outlet", endPoint: OffsetPoint): PipeEnd => {
|
||||
const fallback: PipeEnd = {
|
||||
bottom: endPoint,
|
||||
|
||||
@@ -308,3 +308,39 @@ export function solveWallVertical(
|
||||
const floatGap = Math.max(0, desiredTop - limitHeight - groundBase);
|
||||
return { height, baseElevation: groundBase + floatGap, floatGapM: floatGap };
|
||||
}
|
||||
|
||||
/** 교점을 마감면 **선분 안**으로 자른다 — 선분 밖(벽 하단 아래 등)으로 새지 않게. */
|
||||
export function clampToFace(
|
||||
point: OffsetPoint,
|
||||
face: { base: OffsetPoint; direction: { offset: number; elevation: number } },
|
||||
): OffsetPoint {
|
||||
const dot =
|
||||
(point.offset - face.base.offset) * face.direction.offset +
|
||||
(point.elevation - face.base.elevation) * face.direction.elevation;
|
||||
const len2 =
|
||||
face.direction.offset * face.direction.offset +
|
||||
face.direction.elevation * face.direction.elevation;
|
||||
const t = len2 > 1e-12 ? Math.min(Math.max(dot / len2, 0), 1) : 0;
|
||||
return {
|
||||
offset: face.base.offset + face.direction.offset * t,
|
||||
elevation: face.base.elevation + face.direction.elevation * t,
|
||||
};
|
||||
}
|
||||
|
||||
/** 설계선을 따라 [from → to] 구간의 경사길이(m)를 잰다 — 성토사면 길이 측정용. */
|
||||
export function slopeLengthAlong(
|
||||
designAt: (offset: number) => number,
|
||||
from: number,
|
||||
to: number,
|
||||
): number {
|
||||
const steps = 60;
|
||||
let length = 0;
|
||||
let prevElev = designAt(from);
|
||||
for (let i = 1; i <= steps; i += 1) {
|
||||
const o = from + ((to - from) * i) / steps;
|
||||
const e = designAt(o);
|
||||
length += Math.hypot((to - from) / steps, e - prevElev);
|
||||
prevElev = e;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
@@ -72,8 +72,9 @@ export interface BasinLayout {
|
||||
parts: Array<{ kind: "wall" | "floor"; points: OffsetPoint[] }>;
|
||||
label: OffsetPoint;
|
||||
/** 자동 전환 사유 — 툴팁·라벨에 쓴다(2026-08-22 재정의: 성토사면 ≤3m 기준).
|
||||
* "cut" = 절토측 유입(성토사면 없음), "short" = 성토사면 3m 이하. */
|
||||
reason: "cut" | "short";
|
||||
* "cut" = 절토측 유입(성토사면 없음), "short" = 성토사면 3m 이하,
|
||||
* "manual" = 규칙상 기슭막이 자리지만 사용자가 집수정을 선택. */
|
||||
reason: "cut" | "short" | "manual";
|
||||
/** 집수정이 원지반 안으로 들어갈 때의 절토선(구조물 바깥 끝 → 지반 교차점). */
|
||||
cutLine: { from: OffsetPoint; to: OffsetPoint } | null;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* 우측 상단, 면적표는 중상단이라 이 창은 **좌측 하단**에 둔다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { L } from "./B06_Section_UI_Section_Common";
|
||||
|
||||
/** 조정창이 쓰는 값·동작. 카드가 자기 상태에 맞춰 물려 준다. */
|
||||
@@ -25,6 +25,12 @@ export interface StructurePanelDeps {
|
||||
pipeLengthM: () => number | null;
|
||||
/** 창을 닫는다 = 구조물 선택 해제. */
|
||||
close: () => void;
|
||||
/** 유입측 구조물 형식(드롭다운 값 — 2026-08-22 사용자). */
|
||||
structureFor: () => InletStructureChoice;
|
||||
/** 유입측 구조물 형식 변경 — 카드를 다시 그린다. */
|
||||
setStructure: (value: InletStructureChoice) => void;
|
||||
/** ◀/▶ 이동 가능 여부 — 집수정(자리 고정)은 숨긴다. */
|
||||
canNudge: (key: RevetKey) => boolean;
|
||||
}
|
||||
|
||||
export interface StructurePanelHandle {
|
||||
@@ -92,13 +98,38 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
),
|
||||
);
|
||||
|
||||
// 유입측 구조물 형식 드롭다운(2026-08-22 사용자) — 자동/기슭막이/집수정 I·ㄴ·ㄷ.
|
||||
const structureRow = document.createElement("label");
|
||||
structureRow.className = "b06-structure-panel__struct";
|
||||
const structureLabel = document.createElement("span");
|
||||
structureLabel.textContent = L("B06_Cross_Struct_Label");
|
||||
const select = document.createElement("select");
|
||||
select.className = "b06-structure-panel__select";
|
||||
const OPTIONS: Array<[InletStructureChoice, string]> = [
|
||||
["auto", L("B06_Cross_Struct_Auto")],
|
||||
["revet", L("B06_Cross_Struct_Revet")],
|
||||
["I", L("B06_Cross_Struct_I")],
|
||||
["L", L("B06_Cross_Struct_L")],
|
||||
["U", L("B06_Cross_Struct_U")],
|
||||
];
|
||||
for (const [optionValue, label] of OPTIONS) {
|
||||
const option = document.createElement("option");
|
||||
option.value = optionValue;
|
||||
option.textContent = label;
|
||||
select.append(option);
|
||||
}
|
||||
select.addEventListener("change", () => {
|
||||
deps.setStructure(select.value as InletStructureChoice);
|
||||
});
|
||||
structureRow.append(structureLabel, select);
|
||||
|
||||
const closeButton = makeButton("✕", L("B06_Cross_Revet_Close"), () => deps.close());
|
||||
closeButton.classList.add("b06-structure-panel__close");
|
||||
|
||||
const head = document.createElement("div");
|
||||
head.className = "b06-structure-panel__head";
|
||||
head.append(title, closeButton);
|
||||
root.append(head, value, buttons);
|
||||
root.append(head, value, structureRow, buttons);
|
||||
|
||||
return {
|
||||
root,
|
||||
@@ -106,7 +137,16 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
current = key;
|
||||
root.classList.toggle("is-hidden", key === null);
|
||||
if (!key) return;
|
||||
title.textContent = L(key === "inlet" ? "B06_Cross_Revet_Inlet" : "B06_Cross_Revet_Outlet");
|
||||
title.textContent =
|
||||
key === "outlet"
|
||||
? L("B06_Cross_Revet_Outlet")
|
||||
: deps.canNudge("inlet")
|
||||
? L("B06_Cross_Revet_Inlet")
|
||||
: L("B06_Cross_Struct_InletBasin");
|
||||
// 형식 선택은 유입측에서만, ◀/▶/↺는 자리를 옮길 수 있는 구조물(기슭막이)만.
|
||||
structureRow.classList.toggle("is-hidden", key !== "inlet");
|
||||
if (key === "inlet") select.value = deps.structureFor();
|
||||
buttons.classList.toggle("is-hidden", !deps.canNudge(key));
|
||||
const shift = deps.shiftFor(key);
|
||||
// 자동 자리 기준 이동량 — 0이면 "자동"으로 적어 손을 안 댔음을 바로 알린다.
|
||||
// 부호(+/−)만 적으면 좌·우 벽에서 어느 쪽인지 읽히지 않아 **안/바깥**으로 적는다.
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "./B06_Section_UI_Cross_Design";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { appendCulvertOverlay, computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { InletStructureChoice } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { buildStructurePanel } from "./B06_Section_UI_Cross_Structure_Panel";
|
||||
import { attachZoomPan, buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import type { RevetHighlightSetter, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
@@ -174,6 +175,12 @@ export interface RevetOffsetControl {
|
||||
reset: (chainageM: number, role: RevetKey) => void;
|
||||
}
|
||||
|
||||
/** 유입측 구조물 형식 선택(드롭다운 — 2026-08-22 사용자). 세션 보관은 Page가 한다. */
|
||||
export interface InletStructureControl {
|
||||
valueFor: (section: CrossSection) => InletStructureChoice;
|
||||
set: (chainageM: number, value: InletStructureChoice) => void;
|
||||
}
|
||||
|
||||
export function createCrossSectionCard(
|
||||
section: CrossSection,
|
||||
selected: boolean,
|
||||
@@ -194,6 +201,8 @@ export function createCrossSectionCard(
|
||||
stationWidth?: StationWidthControl,
|
||||
/** 기슭막이 X 자리 제어 — 있으면 벽이 선택 가능해지고 선택 시 ◀/▶/↺가 뜬다. */
|
||||
revetOffset?: RevetOffsetControl,
|
||||
/** 유입측 구조물 형식 선택(드롭다운 — 2026-08-22). */
|
||||
inletStructure?: InletStructureControl,
|
||||
): CrossCardElement {
|
||||
// 이 카드의 실효 표시 반폭 — 개별값이 전역 반폭보다 우선한다(2026-08-06).
|
||||
const effectiveHalfWidth = stationWidth?.widthFor(section) ?? crossHalfWidth;
|
||||
@@ -217,6 +226,8 @@ export function createCrossSectionCard(
|
||||
let showRevetControl: (visible: boolean) => void = () => undefined;
|
||||
/** 지금 그린 관 길이(m) — 조정창이 "관 길이 8m"으로 적는다. 배수관 측점이 아니면 null. */
|
||||
let culvertPipeLengthM: number | null = null;
|
||||
/** 유입측이 집수정인가 — 조정창이 ◀/▶ 표시 여부·제목을 가른다. */
|
||||
let culvertInletIsBasin = false;
|
||||
/**
|
||||
* 기슭막이를 고른다. 구조물 선택과 절·성토 면적 강조는 **같은 레벨**이라 하나를
|
||||
* 고르면 다른 하나는 풀린다(2026-08-21 사용자). 또 구조물을 고르면 그 **측점 카드도
|
||||
@@ -464,6 +475,7 @@ export function createCrossSectionCard(
|
||||
outlet: revetOffset.shiftFor(section, "outlet"),
|
||||
}
|
||||
: undefined,
|
||||
inletStructure?.valueFor(section),
|
||||
);
|
||||
// 포장층 → 설계선 → 암 경계선 순으로 겹쳐, 설계선이 포장 박스 위에 오게 한다.
|
||||
appendPavementOverlay(plotLayer, section.design, x, toDisplayY);
|
||||
@@ -487,6 +499,7 @@ export function createCrossSectionCard(
|
||||
}
|
||||
// 배수관 측점 세트(배관·기슭막이·보호공) — 기존 도형 위에 추가만 한다(2026-08-19).
|
||||
culvertPipeLengthM = culvertLayout?.pipe.lengthM ?? null;
|
||||
culvertInletIsBasin = !!culvertLayout?.basin;
|
||||
// 요청한 이동량이 한계에 걸려 잘렸으면 그 값으로 되돌려 담는다 — 안 그러면 눌러도
|
||||
// 안 움직이는데 창의 숫자만 계속 커진다(2026-08-21 사용자 지적).
|
||||
// 잘린 순간에는 **왜 안 움직였는지 토스트로 알린다**(2026-08-22 사용자 요청 —
|
||||
@@ -604,6 +617,10 @@ export function createCrossSectionCard(
|
||||
close: () => {
|
||||
if (activeRevet) toggleRevet(activeRevet);
|
||||
},
|
||||
structureFor: () => inletStructure?.valueFor(section) ?? "auto",
|
||||
setStructure: (value) => inletStructure?.set(section.chainage_m, value),
|
||||
// 집수정은 자리 고정 — 유입이 집수정이면 ◀/▶/↺를 숨긴다.
|
||||
canNudge: (key) => key === "outlet" || !culvertInletIsBasin,
|
||||
});
|
||||
showRevetControl = (visible) => panel.show(visible ? activeRevet : null);
|
||||
showRevetControl(activeRevet !== null);
|
||||
|
||||
@@ -410,6 +410,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
rockBoundaryControl,
|
||||
stationWidthControl,
|
||||
revetOffsetControl,
|
||||
stationControls.inletStructure,
|
||||
);
|
||||
|
||||
// 메인 영역: 종·횡단 도면(sectionView) 또는 안내 메시지를 표시한다.
|
||||
|
||||
@@ -6,22 +6,27 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { RevetOffsetControl, StationWidthControl } from "./B06_Section_UI_Cross_View";
|
||||
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type {
|
||||
InletStructureControl,
|
||||
RevetOffsetControl,
|
||||
StationWidthControl,
|
||||
} from "./B06_Section_UI_Cross_View";
|
||||
|
||||
/** 제어가 페이지에서 가져다 쓰는 값들 — 클로저 대신 함수로 받아 결합을 끊는다. */
|
||||
export interface StationControlDeps {
|
||||
sessionKey: (kind: "crossw" | "revetx") => string | null;
|
||||
sessionKey: (kind: "crossw" | "revetx" | "inletstruct") => string | null;
|
||||
refreshCard: (chainageM: number) => void;
|
||||
detail: () => SectionDetailResponse | null;
|
||||
crossHalfWidth: () => number | undefined;
|
||||
sampledHalfWidth: () => number;
|
||||
}
|
||||
|
||||
/** 반폭·기슭막이 제어 묶음. `load`는 경로가 바뀔 때 세션 값을 다시 읽는다. */
|
||||
/** 반폭·기슭막이·유입 구조물 제어 묶음. `load`는 경로가 바뀔 때 세션 값을 다시 읽는다. */
|
||||
export interface StationControls {
|
||||
stationWidth: StationWidthControl;
|
||||
revetOffset: RevetOffsetControl;
|
||||
inletStructure: InletStructureControl;
|
||||
widths: Map<string, number>;
|
||||
load: () => void;
|
||||
/** 전체 반영 — 개별 반폭을 전역값으로 덮는다(없으면 비운다). */
|
||||
@@ -166,13 +171,56 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
deps.refreshCard(chainageM);
|
||||
},
|
||||
};
|
||||
/* ── 유입측 구조물 형식(2026-08-22 사용자 — 드롭다운) ────────────────
|
||||
* auto(규칙)/revet(기슭막이+배관)/I/L/U(집수정 형식). 세션에만 담는다. */
|
||||
const inletStructures = new Map<string, InletStructureChoice>();
|
||||
const structSessionKey = (): string | null => deps.sessionKey("inletstruct");
|
||||
|
||||
function loadInletStructures(): void {
|
||||
inletStructures.clear();
|
||||
const key = structSessionKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(key);
|
||||
if (!raw) return;
|
||||
const parsed = JSON.parse(raw) as Record<string, InletStructureChoice>;
|
||||
Object.entries(parsed).forEach(([chainage, value]) => {
|
||||
if (["auto", "revet", "I", "L", "U"].includes(value)) inletStructures.set(chainage, value);
|
||||
});
|
||||
} catch {
|
||||
/* 손상된 세션 값은 무시 — auto(규칙)로 재시작. */
|
||||
}
|
||||
}
|
||||
|
||||
function persistInletStructures(): void {
|
||||
const key = structSessionKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(Object.fromEntries(inletStructures)));
|
||||
} catch {
|
||||
/* 세션 저장 실패는 무시 — 값은 메모리에 유지된다. */
|
||||
}
|
||||
}
|
||||
|
||||
const inletStructureControl: InletStructureControl = {
|
||||
valueFor: (section) => inletStructures.get(section.chainage_m.toFixed(2)) ?? "auto",
|
||||
set: (chainageM, value) => {
|
||||
if (value === "auto") inletStructures.delete(chainageM.toFixed(2));
|
||||
else inletStructures.set(chainageM.toFixed(2), value);
|
||||
persistInletStructures();
|
||||
deps.refreshCard(chainageM);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
stationWidth: stationWidthControl,
|
||||
revetOffset: revetOffsetControl,
|
||||
inletStructure: inletStructureControl,
|
||||
widths: stationWidths,
|
||||
load: () => {
|
||||
loadStationWidths();
|
||||
loadRevetShifts();
|
||||
loadInletStructures();
|
||||
},
|
||||
applyGlobalWidth: (requested, chainages) => {
|
||||
stationWidths.clear();
|
||||
|
||||
@@ -27,6 +27,7 @@ import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design";
|
||||
import type { CrossAreaKey } from "./B06_Section_UI_Cross_Areas";
|
||||
import {
|
||||
createCrossSectionCard,
|
||||
type InletStructureControl,
|
||||
type RevetOffsetControl,
|
||||
crossCardNaturalHeight,
|
||||
type CrossCardElement,
|
||||
@@ -173,6 +174,8 @@ export function createSectionView(
|
||||
stationWidth?: StationWidthControl,
|
||||
/** 기슭막이 X 자리 제어(2026-08-21) — 벽을 골라 0.1m씩 민다. */
|
||||
revetOffset?: RevetOffsetControl,
|
||||
/** 유입측 구조물 형식 선택(2026-08-22) — 조정창 드롭다운. */
|
||||
inletStructure?: InletStructureControl,
|
||||
): SectionViewController {
|
||||
const root = document.createElement("div");
|
||||
root.className = "b06-section";
|
||||
@@ -444,6 +447,7 @@ export function createSectionView(
|
||||
selectArea,
|
||||
stationWidth,
|
||||
revetOffset,
|
||||
inletStructure,
|
||||
);
|
||||
|
||||
/** 범례 버튼 — 곡선 하나를 켜고 끈다. 축은 전체 곡선 기준이라 여기서 움직이지 않는다. */
|
||||
|
||||
@@ -606,6 +606,33 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.b06-structure-panel__buttons.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 유입 구조물 형식 드롭다운(2026-08-22 사용자) — 라벨 + select 한 줄. */
|
||||
.b06-structure-panel__struct {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b06-structure-panel__struct.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b06-structure-panel__select {
|
||||
flex: 1;
|
||||
padding: 1px var(--spacing-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-inputs);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b06-structure-panel__btn {
|
||||
padding: 0 var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
@@ -250,6 +250,13 @@ export const ui_locales_b2 = {
|
||||
B06_Cross_Revet_Reset: ["기슭막이 자동 자리로 초기화", "Reset revetment to solved position"],
|
||||
B06_Cross_Revet_Inlet: ["유입 기슭막이", "Inlet revetment"],
|
||||
B06_Cross_Revet_Outlet: ["유출 기슭막이", "Outlet revetment"],
|
||||
B06_Cross_Struct_Label: ["구조물 형식", "Structure type"],
|
||||
B06_Cross_Struct_Auto: ["자동(규칙)", "Auto (by rule)"],
|
||||
B06_Cross_Struct_Revet: ["기슭막이+배관", "Revetment + pipe"],
|
||||
B06_Cross_Struct_I: ["집수정 I형", "Catch basin — I"],
|
||||
B06_Cross_Struct_L: ["집수정 ㄴ형", "Catch basin — L"],
|
||||
B06_Cross_Struct_U: ["집수정 ㄷ형", "Catch basin — U"],
|
||||
B06_Cross_Struct_InletBasin: ["유입 집수정", "Inlet catch basin"],
|
||||
B06_Cross_Revet_Limit_Inward: [
|
||||
"기슭막이 안쪽 한계 — 노견(도로) 안쪽으로는 들어갈 수 없습니다.",
|
||||
"Revetment inner limit — cannot move inside the road shoulder.",
|
||||
|
||||
Reference in New Issue
Block a user