기슭막이 범위(길이·기준측점 전/후)를 B06 횡단도 조정창에서 지정하게 하고, 그 범위가 덮는 옆 측점의 기슭막이를 "연동" 파생물로 다루도록 체계를 세웠다. 값의 원천은 배수관 옵션(pipe_points) 하나다 — 같은 숫자를 B06 정본에 따로 담으면 B05 배수관 카드와 갈려 어느 쪽이 참인지 알 수 없다. ① 집수정 전/후 필드 신설 — inlet_basin_before_m·after_m(기본 각 1m), 길이 2m 유지. 종방향 구간 계산을 revetSpanOfSpec·basinSpanOfSpec 한 곳으로 모아 2D 링크 판정과 3D 스윕이 같은 값을 쓰게 했다. ② B06 조정창에 길이·전·후 행 — 기슭막이(유입·유출)·집수정 모두. 캐시를 고쳐 즉시 보여주고 저장은 조작이 멎으면 한 번만 나간다(세부유역 재계산이 매번 돌지 않게). 길이를 바꾸면 늘어난 몫만 지금 비율대로 나눠 담는다 — 총길이에서 비율로 다시 계산하면 0.1m 반올림이 쌓여 5.0/5.0이 5.6/5.4로 어긋난다. ③ 링크 카드 선택 허용 — "소유 측점 통째 복사"에서 "위치 4축만 물려받고 원지반 접합은 이 측점 지형으로 재계산"으로 바꿨다. 구조물 형식은 언제나 소유 측점 값이다. 손대기 전에도 어긋나지 않게 소유 측점이 실제로 적용한 4축(revetShift)을 명시값으로 물려준다. ④ 연동(측점별)·경사(전체 공통) 토글 — 연동 조작은 소유 측점 키에 실리고 그 연장이 덮는 측점을 함께 다시 그린다. 경사는 3D 전용 스위치다(끄면 스윕 프레임 dz=0). 횡단도까지 -dz로 밀면 노견에서 출발하는 성토선까지 내려가 노견과 벌어진다. ⑤ 가림 구간 선 정리 — 유입부 접속선과 유출부 접지 후 절토선을 2D·3D 모두에서 빼되 기하는 남긴다. 면적·수량 계산은 건드리지 않았다(전면 개편 대상). ⑥ 집수정은 소유 측점 횡단도 하나에만 — 링크 카드에서 집수정·계류측 다단을 뺀다. ⑦ 3D 구간 겹침 정리 — 측점 간격이 구조물 연장보다 좁으면 두 구간 제어점이 번갈아 서서 짧은 쪽(집수정 2m)이 잘려 보였다. 두 소유 측점의 중간에서 끊는다. ⑧ 조정창 정리 — 십자·9키를 맨 아래로, 창을 위쪽 기준 오버레이로(낮은 횡단도에서 제목이 잘리던 문제), 연동·경사를 1행 토글 버튼으로, 스크롤 자리 유지. 파일 분리: B06_Section_UI_Cross_View_Structure.ts(조정창 배선), B06_Section_Api_Culvert_Options.ts(구간값 저장) — 700줄 제한. BUILD_VERSION 14 → 17(저장 코리도 만료). tsc/ruff/prettier 통과, pytest 196 passed(기존 실패 1건 유지). 작업이력·자체검증 기록: docs/raw/PLAN.md §4-9. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
181 lines
8.3 KiB
TypeScript
181 lines
8.3 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_View_Structure.ts
|
|
* 횡단 카드 ↔ **구조물 조정창** 배선 — 카드 렌더러(`_UI_Cross_View.ts`)에서 700줄
|
|
* 제한으로 분리했다(2026-08-24). 조정창이 부르는 동작을 제어기(기슭막이 4축·유입
|
|
* 구조물·다단·구간값·연동)로 넘기는 얇은 층이고, 기하나 그리기는 하지 않는다.
|
|
* ========================================================================== */
|
|
|
|
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
|
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
|
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
|
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
|
import type { StructurePanelDeps } from "./B06_Section_UI_Cross_Structure_Panel";
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import { L } from "./B06_Section_UI_Section_Common";
|
|
import type {
|
|
ExtraWallControl,
|
|
InletStructureControl,
|
|
RevetLinkControl,
|
|
RevetOffsetControl,
|
|
SpanRole,
|
|
StructureSpanControl,
|
|
} from "./B06_Section_UI_Cross_Culvert_Wire";
|
|
|
|
/**
|
|
* 선택된 벽이 구간값을 갖는 요소인지 — 유입·유출 기슭막이와 유입 집수정만 자기
|
|
* 길이·전/후를 갖는다. 다단(extra·bextra)은 소유 벽 연장을 그대로 따른다.
|
|
*/
|
|
export function spanRoleOf(key: RevetKey, inletIsBasin = false): SpanRole | null {
|
|
if (key === "inlet") return inletIsBasin ? "basin" : "inlet";
|
|
if (key === "outlet") return "outlet";
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 조정창 배선에 필요한 카드 상태·제어기 묶음. 마지막 계산 결과(높이·재질·단 수 등)는
|
|
* 카드가 그릴 때마다 바뀌므로 **값이 아니라 읽는 함수**로 받는다.
|
|
*/
|
|
export interface StructurePanelContext {
|
|
section: CrossSection;
|
|
/**
|
|
* 4축·다단 조작이 실제로 실릴 측점의 누가거리. 연동 중인 링크 카드에서는 **소유
|
|
* 측점**이다 — 연동은 "본 기슭막이를 조절하면 같이 움직인다"는 뜻이므로 조작값이
|
|
* 한 곳에만 있어야 한다(2026-08-24 사용자). 연동을 풀면 이 카드 자신이 된다.
|
|
*/
|
|
adjustChainage: () => number;
|
|
/** 이 카드가 옆 측점에서 연장돼 온 링크 카드인가. */
|
|
isLinked: boolean;
|
|
revetOffset?: RevetOffsetControl;
|
|
inletStructure?: InletStructureControl;
|
|
extraWalls?: ExtraWallControl;
|
|
structureSpan?: StructureSpanControl;
|
|
revetLink?: RevetLinkControl;
|
|
adjustOf: (key: RevetKey) => WallAdjust;
|
|
/** 화면 좌(◀) 방향을 벽 기준 부호로 환산한다. */
|
|
outwardOf: (key: RevetKey) => number;
|
|
heightOfWall: (key: RevetKey) => number;
|
|
materialOfWall: (key: RevetKey) => RevetMaterial;
|
|
/** 마지막 계산의 실제 적용 d(상하) — d 미지정 벽의 ▲▼ 시작값. */
|
|
appliedD: () => Map<RevetKey, number>;
|
|
pipeLengthM: () => number | null;
|
|
inletIsBasin: () => boolean;
|
|
inletOptions: () => { revetAllowed: boolean; basinLUAllowed: boolean };
|
|
extraState: () => { canAdd: boolean; count: number };
|
|
basinExtraState: () => { canAdd: boolean; count: number };
|
|
activeRevet: () => RevetKey | null;
|
|
setActiveRevet: (key: RevetKey | null) => void;
|
|
toggleRevet: (key: RevetKey) => void;
|
|
/** 창 스크롤 자리를 기억할 카드 키(측점 id). */
|
|
scrollKey: string;
|
|
}
|
|
|
|
export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDeps {
|
|
return {
|
|
scrollKey: ctx.scrollKey,
|
|
adjustFor: ctx.adjustOf,
|
|
nudge: (key, screenDeltaM) =>
|
|
ctx.revetOffset?.update(ctx.adjustChainage(), key, {
|
|
x: ctx.adjustOf(key).x + screenDeltaM * ctx.outwardOf(key),
|
|
}),
|
|
// d 미지정(자동)이면 실제 적용 자리에서부터 움직인다(2026-08-23 — d는 0점
|
|
// 기준 절대값이라 시작값이 필요하다).
|
|
nudgeSlope: (key, deltaM) =>
|
|
ctx.revetOffset?.update(ctx.adjustChainage(), key, {
|
|
d: (ctx.adjustOf(key).d ?? ctx.appliedD().get(key) ?? 0) + deltaM,
|
|
}),
|
|
nudgeHeight: (key, deltaM) => {
|
|
const current = ctx.adjustOf(key);
|
|
const base = current.h ?? ctx.heightOfWall(key);
|
|
ctx.revetOffset?.update(ctx.adjustChainage(), key, {
|
|
h: Math.round((base + deltaM) * 10) / 10,
|
|
});
|
|
},
|
|
materialFor: (key) => ctx.materialOfWall(key),
|
|
setMaterial: (key, material) =>
|
|
ctx.revetOffset?.update(ctx.adjustChainage(), key, { m: material }),
|
|
heightFor: (key) => ctx.heightOfWall(key),
|
|
reset: (key) => ctx.revetOffset?.reset(ctx.adjustChainage(), key),
|
|
pipeLengthM: () => ctx.pipeLengthM(),
|
|
close: () => {
|
|
const active = ctx.activeRevet();
|
|
if (active) ctx.toggleRevet(active);
|
|
},
|
|
structureFor: () => ctx.inletStructure?.valueFor(ctx.section) ?? "auto",
|
|
setStructure: (value) => ctx.inletStructure?.set(ctx.adjustChainage(), value),
|
|
basinAdjustFor: () =>
|
|
ctx.inletStructure?.adjustFor(ctx.section) ?? {
|
|
innerWidthM: 1,
|
|
innerHeightM: 1.2,
|
|
lateralM: 0,
|
|
slopeM: 0,
|
|
},
|
|
updateBasin: (patch) =>
|
|
ctx.inletStructure?.updateAdjust(ctx.adjustChainage(), {
|
|
...ctx.inletStructure.adjustFor(ctx.section),
|
|
...patch,
|
|
}),
|
|
// 집수정 좌우는 **화면 기준**으로 받는다(2026-08-22 사용자 확정) — 유입이 우측인
|
|
// 측점에서 ◀가 화면 오른쪽으로 가던 반전을 없앤다. 노견 안쪽(음수)은 금지라
|
|
// 0에서 멈추고, 멈춘 사실은 토스트로 알린다(기슭막이와 같은 규칙).
|
|
nudgeBasinLateral: (screenDeltaM) => {
|
|
const current = ctx.inletStructure?.adjustFor(ctx.section);
|
|
if (!current) return;
|
|
const next = current.lateralM + screenDeltaM * ctx.outwardOf("inlet");
|
|
if (next < -1e-9) {
|
|
showToast(L("B06_Cross_Revet_Limit_Inward"), "info");
|
|
}
|
|
ctx.inletStructure?.updateAdjust(ctx.adjustChainage(), {
|
|
...current,
|
|
lateralM: Math.max(0, Math.round(next * 10) / 10),
|
|
});
|
|
},
|
|
resetBasin: () => ctx.inletStructure?.resetAdjust(ctx.adjustChainage()),
|
|
// 집수정은 자리 고정 — 유입이 집수정이면 ◀/▶/↺를 숨긴다(추가 벽은 항상 이동).
|
|
canNudge: (key) => key !== "inlet" || !ctx.inletIsBasin(),
|
|
optionsFor: () => ctx.inletOptions(),
|
|
extraState: () => ctx.extraState(),
|
|
basinExtraState: () => ctx.basinExtraState(),
|
|
setExtraCount: (count) => {
|
|
// 줄어들며 사라지는 벽이 선택 중이면 선택을 유출 벽으로 옮긴다(빈 대상 방지).
|
|
const active = ctx.activeRevet() ?? "";
|
|
if (
|
|
active.startsWith("extra") &&
|
|
!active.startsWith("bextra") &&
|
|
Number(active.slice(5)) >= count
|
|
) {
|
|
ctx.setActiveRevet("outlet");
|
|
ctx.revetOffset?.select(ctx.section.chainage_m, "outlet");
|
|
}
|
|
ctx.extraWalls?.setCount(ctx.adjustChainage(), count, "outlet");
|
|
},
|
|
setBasinExtraCount: (count) => {
|
|
const activeBasin = ctx.activeRevet() ?? "";
|
|
if (activeBasin.startsWith("bextra") && Number(activeBasin.slice(6)) >= count) {
|
|
ctx.setActiveRevet("inlet");
|
|
ctx.revetOffset?.select(ctx.section.chainage_m, "inlet");
|
|
}
|
|
ctx.extraWalls?.setCount(ctx.adjustChainage(), count, "basin");
|
|
},
|
|
equalizeExtras: () => ctx.extraWalls?.equalize(ctx.adjustChainage()),
|
|
// 구간값(길이·전·후) — 다단은 소유 벽 연장을 따르는 파생물이라 대상이 아니다.
|
|
// 링크 카드에서 만져도 제어기가 소유 측점 값을 고친다(원천은 하나).
|
|
spanFor: (key) => {
|
|
const role = spanRoleOf(key, ctx.inletIsBasin());
|
|
return role && ctx.structureSpan ? ctx.structureSpan.valuesFor(ctx.section, role) : null;
|
|
},
|
|
setSpan: (key, patch) => {
|
|
const role = spanRoleOf(key, ctx.inletIsBasin());
|
|
if (role) ctx.structureSpan?.update(ctx.section, role, patch);
|
|
},
|
|
linkState: () =>
|
|
ctx.isLinked ? { linked: ctx.revetLink?.linkedFor(ctx.section) ?? true } : null,
|
|
setLinked: (linked) => ctx.revetLink?.setLinked(ctx.section.chainage_m, linked),
|
|
followGrade: () =>
|
|
ctx.revetLink?.followGradeFor(ctx.structureSpan?.ownerOf(ctx.section) ?? ctx.section) ?? true,
|
|
setFollowGrade: (follow) => {
|
|
const owner = ctx.structureSpan?.ownerOf(ctx.section) ?? ctx.section;
|
|
ctx.revetLink?.setFollowGrade(owner, follow);
|
|
},
|
|
};
|
|
}
|