refactor(472-3 B06 구조물): 관 · BOX · 세월교 · 물넘이 모양 계산을 logic 으로 옮김(518-2)

- logic 새 파일 9 — B06_Section_Cross_Culvert · _Culvert_Geom · _Culvert_Extra · _Culvert_Tiers ·
  _Culvert_Types · _Culvert_Wire · _Box_Geom · _Ford_Geom · _Ford_Pavement
- 그림 파일(UI_Cross_Culvert · _Culvert_Types · _Box · _Ford · _Ford_Pavement)은 받은 좌표로 그리기만 ·
  관 겹 꼭짓점(pipeShellCorners) · 물넘이 노면 끝 · 월류선 · 유출 성토선은 logic 으로
- 옛 순수 기하 6 파일(UI_Cross_Culvert_Geom · _Extra · _Tiers · _Wire · _Box_Geom · _Ford_Geom) 지움 ·
  부르는 쪽(B05 Corridor · Structure_Layouts · Route_Carry_Patch · Wall 등)은 import 줄만
- test_472_3 GEOMETRY_PULLS 자기 11 줄 + 더는 안 끌리는 Design_Surface 1 줄 걷음
- 시험 · helper 경로를 새 logic 파일로 — 옮긴 파일을 가리키던 낡은 경로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 19:17:23 +09:00
co-authored by Claude Opus 5.5
parent 1d5d63875b
commit e4befd158a
79 changed files with 685 additions and 639 deletions
@@ -22,17 +22,17 @@
import { structureHashParts, wallSweepAt } from "./logic/B05_Profile_UI_Corridor_Structures_Hash";
import type { CrossSection, CulvertSideSpec } from "../B06_Section/B06_Section_Api_Fetch";
import { computeCulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
import { computeCulvertLayout } from "../B06_Section/logic/B06_Section_Cross_Culvert";
import {
culvertOwnerFor,
ownWallsDrawAt,
restrictToSide,
tierSpanOf,
} from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
} from "../B06_Section/logic/B06_Section_Cross_Culvert_Wire";
import { pipeWallThicknessM } from "../B06_Section/logic/B06_Section_UI_Cross_Culvert_Const";
import type { CulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
import { computeFordLayout, fordAdjustOf } from "../B06_Section/B06_Section_UI_Cross_Ford";
import type { FordLayout } from "../B06_Section/B06_Section_UI_Cross_Ford";
import type { CulvertLayout } from "../B06_Section/logic/B06_Section_Cross_Culvert";
import { computeFordLayout, fordAdjustOf } from "../B06_Section/logic/B06_Section_Cross_Ford_Geom";
import type { FordLayout } from "../B06_Section/logic/B06_Section_Cross_Ford_Geom";
import {
boreBandAt,
boreRingChainages,
@@ -40,8 +40,11 @@ import {
splitByBand,
} from "./B05_Profile_UI_Corridor_Structures_Box";
import type { SectionPolygon } from "./B05_Profile_UI_Corridor_Structures_Box";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "../B06_Section/B06_Section_UI_Cross_Box";
import type { BoxAdjust, BoxLayout } from "../B06_Section/B06_Section_UI_Cross_Box";
import {
computeBoxLayout,
DEFAULT_BOX_SIDE_ADJUST,
} from "../B06_Section/logic/B06_Section_Cross_Box_Geom";
import type { BoxAdjust, BoxLayout } from "../B06_Section/logic/B06_Section_Cross_Box_Geom";
import { buildWingSolids } from "./logic/B05_Profile_UI_Corridor_Structures_Wing";
import {
buildWallPath,
@@ -12,8 +12,8 @@
* ========================================================================== */
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
import { computeBoxLayout } from "../B06_Section/B06_Section_UI_Cross_Box";
import type { BoxAdjust } from "../B06_Section/B06_Section_UI_Cross_Box";
import { computeBoxLayout } from "../B06_Section/logic/B06_Section_Cross_Box_Geom";
import type { BoxAdjust } from "../B06_Section/logic/B06_Section_Cross_Box_Geom";
import type { CorridorStructure, StructureFrame } from "./B05_Profile_UI_Corridor_Structures";
import { buildWingSolids } from "./logic/B05_Profile_UI_Corridor_Structures_Wing";
import { chuteBands } from "./B05_Profile_UI_Corridor_Structures_Chute";
@@ -9,13 +9,13 @@
* ========================================================================== */
import type { ChuteSpec, CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
import type { OffsetPoint } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
import type { OffsetPoint } from "../B06_Section/logic/B06_Section_Cross_Culvert_Types";
import {
chuteBandOutline,
chuteRun,
groundLine,
} from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch";
import { outletFillLine } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement";
import { outletFillLine } from "../B06_Section/logic/B06_Section_Cross_Ford_Pavement";
/** 성토 조각 하나 — B06 오버레이가 띠를 그리는 선(절토 조각은 뺀다). */
interface FillSegment {
@@ -11,16 +11,16 @@
* ========================================================================== */
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
import type { CulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
import { computeCulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
import type { CulvertLayout } from "../B06_Section/logic/B06_Section_Cross_Culvert";
import { computeCulvertLayout } from "../B06_Section/logic/B06_Section_Cross_Culvert";
import { bodyAbsolute } from "../B06_Section/B06_Section_UI_Cross_Body_Guard";
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 type { WallAdjust } from "../B06_Section/logic/B06_Section_Cross_Culvert_Types";
import { ZERO_ADJUST } from "../B06_Section/logic/B06_Section_Cross_Culvert_Types";
import {
culvertOwnerFor,
culvertReach,
restrictToSide,
} from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
} from "../B06_Section/logic/B06_Section_Cross_Culvert_Wire";
/** 단면 폴리곤 [offset, 표고][] — 코리도 솔리드가 쓰는 그 형식. */
export type SectionPolygon = Array<[number, number]>;
@@ -24,10 +24,13 @@ import {
inletStructure,
revetOffset,
} from "../B06_Section/B06_Section_Structure_Layouts";
import { DEFAULT_BOX_SIDE_ADJUST } from "../B06_Section/B06_Section_UI_Cross_Box_Geom";
import { DEFAULT_BOX_SIDE_ADJUST } from "../B06_Section/logic/B06_Section_Cross_Box_Geom";
import type { BoxControl } from "../B06_Section/B06_Section_UI_Cross_Box_Panel";
import { culvertLinkFor, type CulvertLink } from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
import { fordAdjustOf } from "../B06_Section/B06_Section_UI_Cross_Ford_Geom";
import {
culvertLinkFor,
type CulvertLink,
} from "../B06_Section/logic/B06_Section_Cross_Culvert_Wire";
import { fordAdjustOf } from "../B06_Section/logic/B06_Section_Cross_Ford_Geom";
import type { FordControl } from "../B06_Section/B06_Section_UI_Cross_Ford_Panel";
import { createCrossSectionCard } from "../B06_Section/B06_Section_UI_Cross_View";
import type { RockBoundaryControl } from "../B06_Section/B06_Section_UI_Cross_Design";
+1 -1
View File
@@ -58,7 +58,7 @@ export type {
} from "@util/common_util_mass_haul_types";
/** 물넘이포장 제원 — 정의처는 렌더 모듈이다(사본 금지, 타입 전용 import라 순환 없음). */
import type { FordPavementSpec } from "./B06_Section_UI_Cross_Ford_Pavement";
import type { FordPavementSpec } from "./logic/B06_Section_Cross_Ford_Pavement";
import type { RevetmentSpec } from "./B06_Section_UI_Cross_Revetment";
import type { PavementEntry } from "@util/common_util_structure_pavement";
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
+5 -5
View File
@@ -16,14 +16,14 @@ import type {
CrossSectionPatch,
SectionDetailResponse,
} from "./B06_Section_Api_Fetch";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import { fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
import { DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./logic/B06_Section_Cross_Culvert_Types";
import type { WallAdjust } from "./logic/B06_Section_Cross_Culvert_Types";
import { fordAdjustOf } from "./logic/B06_Section_Cross_Ford_Geom";
import { DEFAULT_BOX_SIDE_ADJUST } from "./logic/B06_Section_Cross_Box_Geom";
import { PIPE_MATCH_M } from "../common_util/common_util_pipe_match";
import { inferStationInterval } from "../common_util/common_util_svg";
import { applySpanPatch, WALL_MIN_PURE_HEIGHT_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { tierSpanOf } from "./B06_Section_UI_Cross_Culvert_Wire";
import { tierSpanOf } from "./logic/B06_Section_Cross_Culvert_Wire";
import { computeStoredLayouts } from "./B06_Section_Structure_Layouts";
import { ROW_CARRY_KEYS } from "./logic/B06_Section_Station_Swap";
import { mergeCrossPatches } from "./logic/B06_Section_UI_Page_Patches";
+8 -8
View File
@@ -13,25 +13,25 @@
import { computeStructureAreas, wallBodyInFillM2 } from "@util/common_util_cross_structure_areas";
import type { CrossSection } from "./B06_Section_Api_Fetch";
import { withoutGuardedFill } from "./B06_Section_UI_Cross_Body_Guard";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
import type { BoxLayout } from "./B06_Section_UI_Cross_Box_Geom";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./logic/B06_Section_Cross_Box_Geom";
import type { BoxLayout } from "./logic/B06_Section_Cross_Box_Geom";
import { REVET_EMBED_DEPTH_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { groundInterpolator } from "./logic/B06_Section_UI_Cross_Culvert_Solve";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
import type { BasinShape, WallAdjust, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./logic/B06_Section_Cross_Culvert_Types";
import type { BasinShape, WallAdjust, WallLayout } from "./logic/B06_Section_Cross_Culvert_Types";
import {
computeCardCulvert,
culvertLinkFor,
ownWallsDrawAt,
tierSpanOf,
} from "./B06_Section_UI_Cross_Culvert_Wire";
} from "./logic/B06_Section_Cross_Culvert_Wire";
import type {
ExtraWallControl,
InletStructureControl,
RevetOffsetControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
import { computeFordLayout, fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
import type { FordLayout } from "./B06_Section_UI_Cross_Ford_Geom";
} from "./logic/B06_Section_Cross_Culvert_Wire";
import { computeFordLayout, fordAdjustOf } from "./logic/B06_Section_Cross_Ford_Geom";
import type { FordLayout } from "./logic/B06_Section_Cross_Ford_Geom";
import { computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment";
/** 같은 측점으로 볼 누가거리 오차(m) — 정본 반올림 자릿수보다 크게 잡는다. */
@@ -11,8 +11,8 @@
* ========================================================================== */
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
import type { BoxLayout } from "./B06_Section_UI_Cross_Box_Geom";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./logic/B06_Section_Cross_Box_Geom";
import type { BoxLayout } from "./logic/B06_Section_Cross_Box_Geom";
import { FILL_SLOPE_RATIO_MIN } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type {
CulvertDesignTrim,
@@ -20,9 +20,9 @@ import type {
OffsetPoint,
WallAdjust,
WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { computeFordLayout, fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
import type { FordLayout } from "./B06_Section_UI_Cross_Ford_Geom";
} from "./logic/B06_Section_Cross_Culvert_Types";
import { computeFordLayout, fordAdjustOf } from "./logic/B06_Section_Cross_Ford_Geom";
import type { FordLayout } from "./logic/B06_Section_Cross_Ford_Geom";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
type Role = "inlet" | "outlet";
+5 -5
View File
@@ -2,7 +2,7 @@
* B06_Section_UI_Cross_Box.ts
* BOX암거 측점 횡단 카드의 구체(에이프런·저판·상판·내공·성토선) **오버레이 그리기**.
*
* 기하 계산은 `B06_Section_UI_Cross_Box_Geom.ts`가 맡는다(700줄 제한 분리).
* 기하 계산은 `logic/B06_Section_Cross_Box_Geom.ts`가 맡는다(700줄 제한 분리).
* 여기서는 계산 결과(`BoxLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지.
*
* 그리는 순서 = 시공 순서: ① 에이프런 → ② 저판 → ③ 상판 → ④ 내공 윤곽 → ⑤ 성토선.
@@ -10,22 +10,22 @@
* ========================================================================== */
import { pipeTagId, tagShape } from "../common_util/common_util_pick_tag";
import type { BoxLayout, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import type { BoxLayout, BoxSideRole } from "./logic/B06_Section_Cross_Box_Geom";
import type { OffsetPoint } from "./logic/B06_Section_Cross_Culvert_Types";
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
export {
computeBoxLayout,
DEFAULT_BOX_ADJUST,
DEFAULT_BOX_SIDE_ADJUST,
} from "./B06_Section_UI_Cross_Box_Geom";
} from "./logic/B06_Section_Cross_Box_Geom";
export type {
BoxAdjust,
BoxLayout,
BoxSideAdjust,
BoxSideLayout,
BoxSideRole,
} from "./B06_Section_UI_Cross_Box_Geom";
} from "./logic/B06_Section_Cross_Box_Geom";
const SVG_NS = "http://www.w3.org/2000/svg";
@@ -8,7 +8,7 @@
* CSS 클래스와 조작 관례는 세월교·배수관 조정창과 같다.
* ========================================================================== */
import type { BoxSideAdjust, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom";
import type { BoxSideAdjust, BoxSideRole } from "./logic/B06_Section_Cross_Box_Geom";
import type { BoxPick } from "./B06_Section_UI_Cross_Box";
import {
bindArrowKeys,
+13 -48
View File
@@ -2,7 +2,7 @@
* B06_Section_UI_Cross_Culvert.ts
* 배수관 측점 횡단 카드의 배수관 세트(배관·기슭막이·보호공) **오버레이 그리기**.
*
* 기하 계산은 `B06_Section_UI_Cross_Culvert_Geom.ts`가 맡는다(700줄 제한 분리).
* 기하 계산은 `logic/B06_Section_Cross_Culvert_Geom.ts`가 맡는다(700줄 제한 분리).
* 여기서는 계산 결과(`CulvertLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지.
*
* 그리는 순서 = 시공·계산 순서(2026-08-20 사용자 확정):
@@ -15,14 +15,12 @@ import {
REVET_EMBED_DEPTH_M,
FILL_SLOPE_RATIO_MIN,
pipeWallThicknessM,
} from "./B06_Section_UI_Cross_Culvert_Geom";
import type {
BasinShape,
CulvertLayout,
PipeEnd,
WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Geom";
import { appendPipeInfoText, outlineBox } from "./B06_Section_UI_Cross_Culvert_Types";
} from "./logic/B06_Section_Cross_Culvert_Geom";
import type { BasinShape, CulvertLayout, WallLayout } from "./logic/B06_Section_Cross_Culvert_Geom";
import { appendPipeInfoText } from "./B06_Section_UI_Cross_Culvert_Types";
import { outlineBox } from "./logic/B06_Section_Cross_Culvert_Types";
import { pipeShellCorners } from "./logic/B06_Section_Cross_Culvert";
import type { RevetKey, RevetPick } from "./logic/B06_Section_Cross_Culvert";
import { pipeLengthText } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation";
import {
@@ -33,7 +31,7 @@ import {
// 기하 계산 진입점과 공개 상수·타입은 여기서 재수출한다 — B05(최소 토피)와 횡단 뷰가
// 이 모듈 경로로 이미 참조하고 있어 분리 후에도 import 경로를 바꾸지 않는다.
export { MIN_PIPE_COVER_M, computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert_Geom";
export { MIN_PIPE_COVER_M, computeCulvertLayout } from "./logic/B06_Section_Cross_Culvert";
export type {
BasinLayout,
BasinShape,
@@ -41,7 +39,9 @@ export type {
CulvertLayout,
EndFace,
InletStructureChoice,
} from "./B06_Section_UI_Cross_Culvert_Geom";
RevetKey,
RevetPick,
} from "./logic/B06_Section_Cross_Culvert";
const SVG_NS = "http://www.w3.org/2000/svg";
@@ -59,13 +59,6 @@ function polygon(
return shape;
}
/** 기슭막이 선택 키 — 측점 안에서 어느 벽인지 가린다. 추가 벽은 `extra{n}`. */
// "own" = 배관과 무관한 **독립 기슭막이**(구조물 정본 D군). 조정창·조작값 저장은
// 배관 벽과 같은 체계를 그대로 쓴다(2026-08-28 사용자: 배관측점처럼 이동).
export type RevetKey = "inlet" | "outlet" | "own" | `extra${number}` | `bextra${number}`;
/** 고름 전용 키 — 벽 키 + 관(118-1) · 도수로 띠(152-4) — 조정창 없음 · 강조만. 조정 함수에는 `RevetKey` 만. */
export type RevetPick = RevetKey | "pipe" | "chute";
/** 벽 강조를 카드 재생성 없이 갈아 끼우는 setter. */
export type RevetHighlightSetter = (key: RevetPick | null) => void;
@@ -88,7 +81,7 @@ export function appendCulvertOverlay(
/** 옆 측점에서 연동으로 넘어온 벽 — 주 형상과 색을 갈라 그림(79-6). */
linked?: boolean,
): RevetHighlightSetter {
const { culvert, pipe, pipeCorners } = layout;
const { culvert, pipe } = layout;
// 신원표(323-5) — 관 세션 id · 부분 = 조정창 부재키
const ownerId = culvert.chainage_m != null ? pipeTagId(culvert.chainage_m) : null;
const diameter = culvert.diameter_m;
@@ -292,23 +285,6 @@ export function appendCulvertOverlay(
}
// ③ 관 — 축 법선 두께의 직사각형. 끝단면은 구조물 계류측 변에서 잘려 온다(Geom).
const { axis, normal } = layout.pipeAxis;
// 미세한 틈 방지: 양 끝을 축 방향으로 아주 조금만 밀어 넣는다. 벽 전면이 1:0.3으로
// 기울어 있어 크게 밀면 관 하단이 벽 **바깥으로 삐져나온다**(2026-08-20 사용자 ③
// 위치 어긋남 원인) — 렌더링 틈만 덮을 만큼(2㎝)으로 줄였다.
const OVERLAP_M = 0.02;
const pushOut = (corner: PipeEnd, dirSign: number): PipeEnd => ({
bottom: {
offset: corner.bottom.offset + dirSign * axis.offset * OVERLAP_M,
elevation: corner.bottom.elevation + dirSign * axis.elevation * OVERLAP_M,
},
top: {
offset: corner.top.offset + dirSign * axis.offset * OVERLAP_M,
elevation: corner.top.elevation + dirSign * axis.elevation * OVERLAP_M,
},
});
const inletFinal = pushOut(pipeCorners.inlet, -1);
const outletFinal = pushOut(pipeCorners.outlet, 1);
const kindLabel = culvert.pipe_kind ? `${culvert.pipe_kind} ` : "";
const wallThickness = pipeWallThicknessM(culvert.pipe_kind, diameter);
@@ -321,16 +297,6 @@ export function appendCulvertOverlay(
` · 관 두께 ${(wallThickness * 1000).toFixed(1)}㎜(외경선 기준)` +
` · 최소 토피 ${culvert.min_cover_m.toFixed(1)}m(별표2 복토 교차 참조)`;
// 외경(관 두께 바깥) → 내경 순으로 2겹. 도면처럼 관벽 두께가 보이게 한다(사용자 ③).
const shifted = (corner: PipeEnd, delta: number): PipeEnd => ({
bottom: {
offset: corner.bottom.offset - normal.offset * delta,
elevation: corner.bottom.elevation - normal.elevation * delta,
},
top: {
offset: corner.top.offset + normal.offset * delta,
elevation: corner.top.elevation + normal.elevation * delta,
},
});
const pipeShapes: SVGPolygonElement[] = []; // 관 두 겹 — 관 고름 강조(118-1)
let pipeHitPoints: Array<[number, number]> = [];
const pipeLayers = hidePipe
@@ -340,8 +306,7 @@ export function appendCulvertOverlay(
[0, "b06-chart__culvert-pipe"],
] as const);
for (const [delta, cls] of pipeLayers) {
const a = shifted(inletFinal, delta);
const b = shifted(outletFinal, delta);
const [a, b] = pipeShellCorners(layout, delta);
const points: Array<[number, number]> = [
[x(a.bottom.offset), toDisplayY(a.bottom.elevation)],
[x(b.bottom.offset), toDisplayY(b.bottom.elevation)],
@@ -1,55 +1,8 @@
/* =============================================================================
* B06_Section_UI_Cross_Culvert_Types.ts
* 배수관 세트 기하 **자료형** — 기하 계산(`_Cross_Culvert_Geom.ts`)에서 분리했다
* (700줄 제한). 계산·그리기 양쪽이 함께 쓴다.
* 배수관 · 세월교 관 정보 글 그리기 — 자료형은 `logic/B06_Section_Cross_Culvert_Types.ts`.
* ========================================================================== */
import type { CulvertSet } from "./B06_Section_Api_Fetch";
import type { RevetMaterial } from "./logic/B06_Section_UI_Cross_Culvert_Const";
/**
* 기슭막이 1매의 사용자 조작값(2026-08-22 확정 4축):
* x = 좌우(m, + = 계류측 바깥) — 노견(또는 윗단 시작점 수평선)을 늘려 평행이동,
* 성토선 물매 1:1.2 불변.
* d = 상하(m, 수평 성분, + = 사면 아래) — 성토선을 타고 대각 이동.
* **배관(유입·유출) 벽**은 0점 = 성토선 길이 0 지점(벽 상단 = 노견)인
* 절대값(2026-08-23 사용자 확정), null = 기본 자리(성토선 0 지점 · 79-5).
* 추가 벽은 종전대로 자동 자리 기준 상대값(null = 0).
* h = 높이(m). 배관 벽은 **순수 높이**(바닥~상단, 관 아래 0.5 포함 —
* 2026-08-23), 추가 벽은 계산용(근입 0.5 위 기준선~상단). null = 기본값
* (배관 관경+1.0·일반 1.5, 재질 한계로 잘림).
* m = 재질. null = 기본값(메쌓기). 한계: 메 2.0 / 찰 3.0 / 콘크리트 5.0.
*/
export interface WallAdjust {
x: number;
d: number | null;
h: number | null;
/** 형태(`REVET_FORMS` 문자열). 옛 저장분은 재질 코드(dry/wet/concrete) — `revetFormOf`가 옮긴다. */
m: string | null;
}
/** 조작값 기본(전부 자동). */
export const ZERO_ADJUST: WallAdjust = { x: 0, d: null, h: null, m: null };
export interface OffsetPoint {
offset: number;
elevation: number;
}
/** 점들을 감싸는 가상 사각형 — 고르기 판정면만(그림 아님) · 세월교 측벽 ㄴ(79-6) · 집수정(190-1). */
export function outlineBox(points: OffsetPoint[]): OffsetPoint[] {
const offsets = points.map((point) => point.offset);
const elevations = points.map((point) => point.elevation);
const [left, right] = [Math.max(...offsets), Math.min(...offsets)];
const [top, bottom] = [Math.max(...elevations), Math.min(...elevations)];
return [
{ offset: left, elevation: top },
{ offset: right, elevation: top },
{ offset: right, elevation: bottom },
{ offset: left, elevation: bottom },
];
}
/**
* 관 정보 글(세월교 · 관 자리) — 노폭 · 노견 라벨(`.b06-chart__carriageway-label`) 바로 위 줄(225-1).
* 같은 그림에 그 라벨이 없으면(B07 덧그림) `fallback` 자리 · fallback 도 없으면 안 그림.
@@ -74,198 +27,3 @@ export function appendPipeInfoText(
label.textContent = text;
(road?.parentNode ?? layer).appendChild(label);
}
/** 설계선(성토 경사) 트림 범위 — 이 밖은 벽·보호공이 대신하므로 그리지 않는다. */
export interface CulvertDesignTrim {
minOffset: number;
maxOffset: number;
/** 트림 경계에서 성토 경사선이 **닿아야 할 표고**(= 벽 이음선 상단점). 없으면 설계선 그대로. */
minElevation?: number;
maxElevation?: number;
/**
* 노견 → (노폭 연장) → 벽 이음선 상단점 폴리라인. 물매가 1:1.2 고정이라 벽이 밖으로
* 나간 만큼 **노폭이 늘어난다** — 그 연장 구간이 가운데 점으로 들어온다.
*/
minSlope?: { points: OffsetPoint[] };
maxSlope?: { points: OffsetPoint[] };
}
export interface WallLayout {
role: "inlet" | "outlet" | "extra";
/** 추가 기슭막이 번호(role === "extra"일 때만) — 조정창 키 `extra{n}`이 된다. */
extraIndex?: number;
form: string | null;
lengthM: number | null;
/** 배면(도로측) 바닥 offset. */
backOffset: number;
/** 전면(계류측) 바닥 offset. */
outerOffset: number;
base: number;
/** 벽 높이(m) = **계산용**(관 invert~상단, 근입 제외 — 2026-08-22 사용자 확정).
* 기초는 시공 시 묻히는 부분이라 높이 표기·한계 검사(찰 3.0/메 2.0) 모두 이 기준. */
height: number;
/**
* 바닥이 원지반에서 뜬 간격(m, 2026-08-22 사용자 확정). 손으로 안쪽에 당겨 높이
* 한계로도 사면선에 못 닿을 때 바닥을 띄워 최소 노견을 확보한다 — 뜬 공간은
* 별도 지지 구조물로 메운다(추가 예정). 0이면 지반 위(근입 0.5m).
*/
floatGapM: number;
/** 좌우 이동의 **하한**(m, 2026-09-07). 선반 길이가 음수가 되지 않게 x 는 최소
* `1.2 × 상하 내림`까지 따라 나간다. 요청이 이 값보다 작으면 눌러도 벽이 안 움직임 —
* 그 까닭을 툴팁으로 알린다. */
shiftFloorM?: number;
/** 재질(메/찰/콘크리트) — 높이 한계를 정한다(2026-08-22 사용자). */
material: RevetMaterial;
/** 전면 기울기 1:n 의 n — 품셈 13-4-4 [주]⑪ 표준경사(`wallLeanRatio`, B08 과 한 벌 · 2026-09-14). */
lean: number;
outward: number;
/** 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점 — 데이터 좌표. */
points: OffsetPoint[];
/** 상단 배면 꼭짓점 — 성토 사면선과의 접점(설계선 트림 경계). */
topBack: OffsetPoint;
/** 이음선 상단점(평행사변형↔사다리꼴 상단 교차점) — 안 무늬 이음선 자리. */
topJoint: OffsetPoint;
/** 성토 위 이음 — 윗면 사면 쪽 끝에서 0.1 m 안(371-1). 성토 사면선이 여기서 끝난다. */
fillTop: OffsetPoint;
/** 하단선 양 끝 — **수평 기초**(2026-08-22 사용자 확정): 발자국 최저 지반(또는 관
* invert) − 0.5m 표고의 수평선. 전면 발끝은 1:0.3 경사선을 하단까지 연장한 자리다. */
bottomBack: OffsetPoint;
bottomFront: OffsetPoint;
/** 옹벽(구조물 `retaining_wall`) — 안 무늬에 물빼기 파이프 단면을 더함(73-2). */
retaining?: boolean;
}
/**
* 유입 집수정 단면 — 실무 도면(2026-08-20 사용자 제공 L형 확대본) 기준.
* · **I형** = 기슭막이와 같은 **평행사변형 벽 1매**만.
* · **ㄴ(L)형** = I형 + **바닥판**.
* · **ㄷ형** = ㄴ형 + I형 **반대측 막음벽**.
* 돌 표현은 재질 확정 시 반영 예정(성토부 기슭막이와 동일 규칙) — 지금은 윤곽만.
*/
export type BasinShape = "I" | "L" | "U";
export interface BasinLayout {
shape: BasinShape;
/** 부재별 폴리곤(벽·바닥·막음벽). 각각 분리 도형으로 그린다. */
parts: Array<{ kind: "wall" | "floor"; points: OffsetPoint[] }>;
label: OffsetPoint;
/** 자동 전환 사유 — 툴팁·라벨에 쓴다(2026-08-22 재정의: 성토사면 ≤3m 기준).
* "cut" = 절토측 유입(성토사면 없음), "short" = 성토사면 3m 이하,
* "manual" = 규칙상 기슭막이 자리지만 사용자가 집수정을 선택. */
reason: "cut" | "short" | "manual";
/** 집수정이 원지반 안으로 들어갈 때의 절토선 점열(구조물 바깥 끝 → 복합경사 → 지반 교차점 · 91-4). */
cutLine: OffsetPoint[] | null;
/** 구조물이 원지반 밖으로 나올 때의 성토(되메움)선 — 경사 5도(임시), 지반 교차점까지. */
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;
direction: { offset: number; elevation: number };
}
/** 관 한쪽 끝단의 두 꼭짓점(마감면에서 자른 결과). */
export interface PipeEnd {
bottom: OffsetPoint;
top: OffsetPoint;
}
/**
* 유출측 **성토부선** 한 구간(2026-08-22 사용자 — 보호공 삭제, 윗면 선만 성토부선으로).
* 관 하단 꼭짓점(또는 앞 추가 벽 전면 상단)에서 1:1.2로 원지반(또는 다음 벽)까지.
*/
export interface OutletFillSegment {
points: OffsetPoint[];
lengthM: number;
/** 구간 물매(1:n) — 벽 사이 구간은 높이 역산으로 정확히 1.2다. */
ratio?: number;
/** 선 성격(2026-08-22 사용자 ②③): fill = 성토선(기본), cut = 0도/접속 절토선. */
kind?: "fill" | "cut";
/** 사면길이 5m 이상 — 기슭막이 의무 구간(별표2·성토_비탈면 §2). */
overLimit: boolean;
}
export interface CulvertLayout {
culvert: CulvertSet;
/** 원지반 — 도수로 · 돌붙임 길이가 사면보다 길면 이어 그림(294-4). */
ground?: OffsetPoint[];
pipe: { inlet: OffsetPoint; outlet: OffsetPoint; lengthM: number; slopePct: number };
/** 관 축·법선 단위벡터(그리기가 두께 방향으로 밀 때 쓴다). */
pipeAxis: { axis: OffsetPoint; normal: OffsetPoint };
/** 관 양 끝단 꼭짓점 — 보호공 시작점이 유출 하단 꼭짓점을 그대로 쓴다. */
pipeCorners: { inlet: PipeEnd; outlet: PipeEnd };
walls: WallLayout[];
/** 유출측 성토사면 — 경사길이(m)·법정 5m 충족 여부(별표2)·풀어낸 물매(1:n). */
fillSlope: {
lengthM: number;
withinLimit: boolean;
/** 사면선이 벽 이음선 상단점을 지나도록 역산한 물매. 범위 밖이면 끝값으로 고정. */
ratio: number;
/** 물매가 1.2~2.0 범위를 벗어났는지 — 도면 경고용. */
ratioClamped: boolean;
/** 물매 1:1.2를 지키려고 늘린 **노폭 연장량**(m, 편측). 0이면 연장 없음. */
roadWideningM: number;
/**
* 기슭막이만으로는 성토를 받을 수 없는 자리(성토고 3m↑인데 사면 5m·물매 1:1.2를
* 동시에 만족하는 벽 자리가 없다) — **옹벽·석축 등 구조물 검토 대상**
* (`성토_비탈면.md` §2 "5m 초과 시 구조물 설치 의무").
*/
structureRequired: boolean;
};
/** 유출측 성토부선(보호공 대체 — 2026-08-22). 끝 구간이 5m 이상이면 추가 가능. */
outletFill: { segments: OutletFillSegment[]; addable: boolean };
/** 유입 집수정(ㄴ·ㄷ형) 계류측 성토부선 — 유출측과 같은 체계(2026-08-22 사용자). */
basinFill: { segments: OutletFillSegment[]; addable: boolean };
/** 집수정 계류측 다단 기슭막이 — 조정창 키 `bextra{n}`. */
basinExtras: WallLayout[];
/** 한계에 잘린 뒤의 **실제 적용된** 집수정 조작값 — 조정창이 되받는다. */
basinAdjust: BasinAdjust;
/** 유입 기슭막이의 관 시작 접속선(2026-08-22 ②) — 위면 0도 성토선(지반 정지),
* 아래면 0도 1m 후 표준 절토 경사선. 집수정·접속 불필요면 null. */
inletFill: OutletFillSegment | null;
/** 추가 기슭막이(배관 없는 벽 — 계단식). 조정창 키 = `extra{n}`. */
extraWalls: WallLayout[];
/** 유입 집수정 단면(있을 때만). */
basin: BasinLayout | null;
designTrim: CulvertDesignTrim | null;
/**
* **실제 적용된** 기슭막이 이동량(m, + = 계류측 바깥). 요청값이 노견·사면 역전·사면 끝
* 한계에 걸리면 잘려 들어온다 — 조정창은 이 값을 보여 주고 저장한다. 그러지 않으면
* 눌러도 안 움직이는데 숫자만 계속 커진다(2026-08-21 사용자 지적).
*/
revetShift: {
inlet: WallAdjust;
outlet: WallAdjust;
extras: WallAdjust[];
/** 집수정 계류측 다단의 적용 조작값. */
basinExtras: WallAdjust[];
};
/**
* 조정창 드롭다운 선택지 가용성(2026-08-22 사용자 — 상황에 안 맞는 선택지 숨김):
* revetAllowed = 기슭막이+배관(배관이 원지반에 안 묻힐 때만),
* basinLUAllowed = 집수정 ㄴ·ㄷ형(구조물이 원지반으로 밀고 들어갈 때만).
*/
inletOptions: { revetAllowed: boolean; basinLUAllowed: boolean };
/** 세월교 · 박스 흙막이(73-4) — 본체 끝 자리의 배관 4축 x(적용 x 의 0점). 본체 없으면 없음. */
bodyX?: { inlet: number; outlet: number } | null;
/** 유입 · 유출 벽의 순수 높이(m) — 높이 빈 칸인 연동 옆 카드가 물려받음(152-2). */
pureHeights?: { inlet: number; outlet: number };
}
/** 유입측 구조물 사용자 선택 — auto = 규칙 기반 자동 선택. */
export type InletStructureChoice = "auto" | "revet" | "I" | "L" | "U";
@@ -20,7 +20,7 @@ import {
WALL_FOUNDATION_WIDTH_M,
WALL_TRENCH_CLEARANCE_M,
} from "@util/common_util_excavation";
import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import type { WallLayout } from "./logic/B06_Section_Cross_Culvert_Types";
const SVG_NS = "http://www.w3.org/2000/svg";
+6 -9
View File
@@ -2,7 +2,7 @@
* B06_Section_UI_Cross_Ford.ts
* 세월교 측점 횡단 카드의 구체(측벽·바닥판·관·성토 채움) **오버레이 그리기**.
*
* 기하 계산은 `B06_Section_UI_Cross_Ford_Geom.ts`가 맡는다(700줄 제한 분리).
* 기하 계산은 `logic/B06_Section_Cross_Ford_Geom.ts`가 맡는다(700줄 제한 분리).
* 여기서는 계산 결과(`FordLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지.
*
* 그리는 순서 = 시공 순서: ① 바닥판 → ② 측벽(ㄴ형 집수정 재사용) → ③ 관 →
@@ -10,12 +10,9 @@
* ========================================================================== */
import { pipeTagId, tagShape } from "../common_util/common_util_pick_tag";
import type { FordLayout, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom";
import {
appendPipeInfoText,
outlineBox,
type OffsetPoint,
} from "./B06_Section_UI_Cross_Culvert_Types";
import type { FordLayout, FordWallRole } from "./logic/B06_Section_Cross_Ford_Geom";
import { appendPipeInfoText } from "./B06_Section_UI_Cross_Culvert_Types";
import { outlineBox, type OffsetPoint } from "./logic/B06_Section_Cross_Culvert_Types";
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
export {
@@ -24,14 +21,14 @@ export {
DEFAULT_FORD_WALL_ADJUST,
fordAdjustOf,
fordDropOf,
} from "./B06_Section_UI_Cross_Ford_Geom";
} from "./logic/B06_Section_Cross_Ford_Geom";
export type {
FordAdjust,
FordLayout,
FordSideLayout,
FordWallAdjust,
FordWallRole,
} from "./B06_Section_UI_Cross_Ford_Geom";
} from "./logic/B06_Section_Cross_Ford_Geom";
const SVG_NS = "http://www.w3.org/2000/svg";
@@ -12,7 +12,7 @@
* 바닥판 연장은 날개벽 길이×cos(각도)가 정하므로 읽기 전용으로 보여 준다.
* ========================================================================== */
import type { FordAdjust, FordWallAdjust, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom";
import type { FordAdjust, FordWallAdjust, FordWallRole } from "./logic/B06_Section_Cross_Ford_Geom";
import type { FordPick } from "./B06_Section_UI_Cross_Ford";
import {
bindArrowKeys,
@@ -12,37 +12,13 @@
* ========================================================================== */
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import type { OffsetPoint } from "./logic/B06_Section_Cross_Culvert_Types";
import { fordEdges, outletFillLine, overflowLine } from "./logic/B06_Section_Cross_Ford_Pavement";
import type { FordPavementSpec } from "./logic/B06_Section_Cross_Ford_Pavement";
import { appendPavementOverlay } from "./B06_Section_UI_Cross_Design_Surface";
import { pipeTagId, tagShape } from "../common_util/common_util_pick_tag";
import { appendChuteBand, chuteRun } from "./B06_Section_UI_Cross_Wall_Hatch";
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
export interface FordPavementSpec {
/** 관 지점 누가거리 — 폭 절반 걸친 옆 카드도 같은 값. */
chainage_m?: number;
span_m: number;
/** 월류 높이(m) — 노면 위 점선. 없으면 점선을 안 그림(수치를 지어내지 않는다). */
depth_m: number | null;
/** 노면 경사(%) — 경사각도 환산 · 서버 설계 노면이 이 값으로 기욺. */
slope_pct: number | null;
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
defaulted?: string[];
/** 도수로 · 돌붙임(`ford_chute_{n}_*` · 136-2) — 유출측 성토사면 띠 · 길이 = 사면 따라(286-3 · 비면 원지반까지). */
chute?: { structure: string; thickness_m: number; length_m?: number };
}
/** 유입(상류)이 좌측(+offset)인가 — 기울인 노면이면 높은 노면 끝 · 아니면 설계선 양 끝 표고가 높은
* 쪽(286-2) · 같으면 단면유형. */
export function inflowLeft(design: CrossDesign): boolean {
const edges = design.road_edges;
if (design.tilted && edges && Math.abs(edges.left.elevation_m - edges.right.elevation_m) > 1e-6)
return edges.left.elevation_m > edges.right.elevation_m;
const line = [...(design.design_line ?? [])].sort((a, b) => a.offset_m - b.offset_m);
const gap = line.length >= 2 ? line[line.length - 1].elevation_m - line[0].elevation_m : 0;
return Math.abs(gap) > 1e-3 ? gap > 0 : design.section_mode !== "right_cut";
}
function line(points: string[], className: string): SVGPolylineElement {
const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
polyline.setAttribute("points", points.join(" "));
@@ -134,90 +110,6 @@ export function appendFordPavementOverlay(
return true;
}
/** 콘크리트 · 노면 양 끝 — 낮은 쪽을 늘였으면(294-3 `tilt_reach`) 그 쪽은 원지반과 만난 자리까지. */
export function fordEdges(design: CrossDesign): CrossDesign["road_edges"] | undefined {
const edges = design.road_edges;
const reach = design.tilt_reach;
if (!edges || !reach) return edges;
const end = { offset_m: reach.offset_m, elevation_m: reach.elevation_m };
return reach.side === "left" ? { ...edges, left: end } : { ...edges, right: end };
}
/**
* 월류 높이선(315-2) — 노면 양 끝 위 월류 높이 · 노면과 같은 각도. 성토 쪽(노면 끝 밖 설계선이 내려감)은
* 성토 사면선을 노면 끝 너머 위 · 안쪽으로 늘여 월류 높이선과 만나는 자리에서 끝 → 노면 끝까지 그 연장선.
* 절토 쪽 · 늘인 노면 끝(294-3)은 노면 끝 위 그대로. 좌(+offset) → 우 차례.
*/
export function overflowLine(design: CrossDesign, depth: number): OffsetPoint[] {
const edges = fordEdges(design);
if (!edges) return [];
const { left, right } = edges;
const span = Math.abs(left.offset_m - right.offset_m);
const side = (edge: typeof left, other: typeof left): OffsetPoint[] => {
const top = { offset: edge.offset_m, elevation: edge.elevation_m + depth };
const outward = Math.sign(edge.offset_m - other.offset_m) || 1;
const next = (design.design_line ?? [])
.filter((p) => (p.offset_m - edge.offset_m) * outward > 1e-6)
.sort((a, b) => (a.offset_m - b.offset_m) * outward)[0];
const cut = edge.offset_m === design.tilt_reach?.offset_m; // 늘인 끝 = 원지반(294-3 · 절토)
if (cut || !next || next.elevation_m >= edge.elevation_m - 1e-6 || span < 1e-9) return [top];
// 안쪽 d m 에서 성토선 = edge + fill·d · 월류선 = edge + depth + road·d → d = depth / (fill − road)
const fill = (edge.elevation_m - next.elevation_m) / Math.abs(next.offset_m - edge.offset_m);
const road = (other.elevation_m - edge.elevation_m) / span;
if (fill <= road + 1e-9) return [top];
const d = Math.min(depth / (fill - road), span);
const meet = { offset: edge.offset_m - outward * d, elevation: edge.elevation_m + fill * d };
return [{ offset: edge.offset_m, elevation: edge.elevation_m }, meet];
};
return [...side(left, right), ...side(right, left).reverse()];
}
/** 원지반 꺾은선의 그 오프셋 표고(밖은 끝값). */
function groundZ(ground: readonly OffsetPoint[], offset: number): number | null {
if (!ground.length) return null;
if (offset <= ground[0].offset) return ground[0].elevation;
for (let i = 1; i < ground.length; i += 1) {
const [a, b] = [ground[i - 1], ground[i]];
if (offset > b.offset) continue;
const span = b.offset - a.offset;
return span > 1e-9
? a.elevation + ((b.elevation - a.elevation) * (offset - a.offset)) / span
: b.elevation;
}
return ground[ground.length - 1].elevation;
}
/**
* 유출측(낮은 쪽) 성토사면 선(136-2 · 294-4) — 낮은 쪽 끝단(늘인 노면 끝 · 아니면 유출 쪽 노견 끝)에서
* 설계선을 바깥으로 따라 내려가는 동안만(오르면 절토라 멈춤) · 원지반(`ground`)에 닿으면 끝.
* 성토가 없으면(끝단이 이미 원지반 · 양측 절토 물넘이) 점 하나 = 띠 없음.
*/
export function outletFillLine(
design: CrossDesign,
ground?: readonly OffsetPoint[],
): OffsetPoint[] {
const edges = fordEdges(design);
if (!edges || !design.design_line?.length) return [];
const reach = design.tilt_reach;
const outLeft = reach ? reach.side === "left" : !inflowLeft(design); // +offset = 좌측
const outward = outLeft ? 1 : -1;
const edge = outLeft
? Math.max(edges.left.offset_m, edges.right.offset_m)
: Math.min(edges.left.offset_m, edges.right.offset_m);
const points = design.design_line
.filter((p) => (p.offset_m - edge) * outward >= -1e-6)
.sort((a, b) => (a.offset_m - b.offset_m) * outward);
const line: OffsetPoint[] = [];
for (const p of points) {
const last = line[line.length - 1];
if (last && p.elevation_m > last.elevation + 1e-6) break;
line.push({ offset: p.offset_m, elevation: p.elevation_m });
const z = ground ? groundZ(ground, p.offset_m) : null;
if (z !== null && Math.abs(z - p.elevation_m) < 1e-3) break; // 원지반에 닿음 = 끝
}
return line;
}
/**
* 물넘이 도수로 · 돌붙임 선(286-3 · 294-4) — 낮은 쪽 끝단에서 성토사면 따라 · 길이 비움 = 원지반 닿으면 끝 ·
* 길이 넣음 = 그 길이(사면보다 길면 원지반 따라 이어 · 315-1 이은 조각까지 한 선) · 양측 절토(늘인 노면 끝이
@@ -29,14 +29,14 @@ import { outlineLeanOf } from "./logic/B06_Section_UI_Cross_Lean";
import { catalogRow } from "../common_util/common_util_structure_catalog";
import type { RevetMaterial } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { fillWallBaseWidth, groundInterpolator } from "./logic/B06_Section_UI_Cross_Culvert_Solve";
import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
import { buildExtrasAt } from "./logic/B06_Section_Cross_Culvert_Extra";
import {
ZERO_ADJUST,
type CulvertDesignTrim,
type OutletFillSegment,
type WallAdjust,
type WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Types";
} from "./logic/B06_Section_Cross_Culvert_Types";
import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation";
import { appendPlanLine, buildRevetWallGeometry, drawRevetWall } from "./B06_Section_UI_Cross_Wall";
@@ -16,8 +16,8 @@
import type { InletStructureChoice, RevetKey, RevetPick } from "./B06_Section_UI_Cross_Culvert";
import { REVET_FORMS } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { BasinAdjust, WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import type { SpanValues } from "./B06_Section_UI_Cross_Culvert_Wire";
import type { BasinAdjust, WallAdjust } from "./logic/B06_Section_Cross_Culvert_Types";
import type { SpanValues } from "./logic/B06_Section_Cross_Culvert_Wire";
import { L } from "./B06_Section_UI_Section_Common";
import {
arrowsBelongToProfile,
@@ -13,7 +13,7 @@
* ========================================================================== */
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import type { OffsetPoint } from "./logic/B06_Section_Cross_Culvert_Types";
import { cell, courses, type BandFrame } from "./B06_Section_UI_Cross_Wall_Hatch";
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
+5 -5
View File
@@ -19,7 +19,7 @@ import {
appendFordPavementOverlay,
appendFordSurfaceDropPlan,
} from "./B06_Section_UI_Cross_Ford_Pavement";
import { ownWallsDrawAt } from "./B06_Section_UI_Cross_Culvert_Wire";
import { ownWallsDrawAt } from "./logic/B06_Section_Cross_Culvert_Wire";
import { attachShapePick, shapePickRoute, splitPipePick } from "./B06_Section_UI_Cross_Shape_Pick";
import { stackPickTargets } from "./B06_Section_UI_Cross_Pick_Order";
import { appendUnderdrainOverlay } from "./B06_Section_UI_Cross_Underdrain";
@@ -46,16 +46,16 @@ import {
} from "./B06_Section_UI_Cross_View_Frame";
import { appendFordOverlay, computeFordLayout } from "./B06_Section_UI_Cross_Ford";
import type { FordControl } from "./B06_Section_UI_Cross_Ford_Panel";
import { computeCardCulvert, setCulvertNotice } from "./B06_Section_UI_Cross_Culvert_Wire";
import { computeCardCulvert, setCulvertNotice } from "./logic/B06_Section_Cross_Culvert_Wire";
import { showToast } from "@ui/ui_template_elements";
import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
import type { CulvertLink } from "./logic/B06_Section_Cross_Culvert_Wire";
import type {
ExtraWallControl,
InletStructureControl,
RevetLinkControl,
RevetOffsetControl,
StructureSpanControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
} from "./logic/B06_Section_Cross_Culvert_Wire";
import { culvertCardState } from "./B06_Section_UI_Cross_View_Structure";
import { buildCardPanels, emptyCardCulvertState } from "./B06_Section_UI_Cross_View_Panels";
import {
@@ -134,7 +134,7 @@ export type {
SpanRole,
SpanValues,
StructureSpanControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
} from "./logic/B06_Section_Cross_Culvert_Wire";
export function createCrossSectionCard(
section: CrossSection,
@@ -7,7 +7,7 @@
import type { CrossSection } from "./B06_Section_Api_Fetch";
import type { RevetKey, RevetPick } from "./B06_Section_UI_Cross_Culvert";
import { REVET_FORM_DEFAULT } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import type { WallAdjust } from "./logic/B06_Section_Cross_Culvert_Types";
import type {
CulvertLink,
ExtraWallControl,
@@ -15,7 +15,7 @@ import type {
RevetLinkControl,
RevetOffsetControl,
StructureSpanControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
} from "./logic/B06_Section_Cross_Culvert_Wire";
import { buildStructurePanel } from "./B06_Section_UI_Cross_Structure_Panel";
import { adoptAdjustPanel, releaseAdjustPanel } from "./B06_Section_UI_Adjust_Dock";
import { structurePanelDeps } from "./B06_Section_UI_Cross_View_Structure";
@@ -15,8 +15,8 @@ import {
WALL_MIN_PURE_HEIGHT_M,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { clampAxis, type AxisDragSpec } from "./B06_Section_UI_Cross_Panel_Base";
import type { CulvertLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import type { CulvertLayout } from "./logic/B06_Section_Cross_Culvert_Types";
import type { WallAdjust } from "./logic/B06_Section_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";
@@ -27,7 +27,7 @@ import type {
RevetOffsetControl,
SpanRole,
StructureSpanControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
} from "./logic/B06_Section_Cross_Culvert_Wire";
/** 다단(추가) 벽 키인가 — 유출측 `extra0…`·유입측 `bextra0…`. */
export const isTierKey = (key: RevetKey): boolean =>
+1 -1
View File
@@ -18,7 +18,7 @@ import {
WALL_MIN_PURE_HEIGHT_M,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { OffsetPoint, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import type { OffsetPoint, WallLayout } from "./logic/B06_Section_Cross_Culvert_Types";
import { appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
const SVG_NS = "http://www.w3.org/2000/svg";
@@ -17,7 +17,7 @@
* ========================================================================== */
import { REVET_THICKNESS_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { OffsetPoint, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import type { OffsetPoint, WallLayout } from "./logic/B06_Section_Cross_Culvert_Types";
const SVG_NS = "http://www.w3.org/2000/svg";
const HATCH_CLASS = "b06-chart__culvert-stone";
@@ -7,7 +7,7 @@
import type { CrossSection, StoredWallSpan } from "./B06_Section_Api_Fetch";
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import type { WallAdjust } from "./logic/B06_Section_Cross_Culvert_Types";
import {
computeCardCulvert,
culvertLinkFor,
@@ -20,7 +20,7 @@ import {
type RevetLinkControl,
type RevetOffsetControl,
type SpanValues,
} from "./B06_Section_UI_Cross_Culvert_Wire";
} from "./logic/B06_Section_Cross_Culvert_Wire";
import { culvertCardState } from "./B06_Section_UI_Cross_View_Structure";
import type { PipeRevetChoice } from "../A00_Common/design/design_structure_pipe_calc";
@@ -12,7 +12,7 @@ import {
tierSpanOf,
type SpanValues,
type StructureSpanControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
} from "./logic/B06_Section_Cross_Culvert_Wire";
import * as CulvertConst from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { SessionCells } from "./B06_Section_Session_Cells";
@@ -12,8 +12,8 @@ import type {
StoredWallSpan,
} from "./B06_Section_Api_Fetch";
import type { InletStructureChoice, RevetKey, RevetPick } 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";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./logic/B06_Section_Cross_Culvert_Types";
import type { BasinAdjust, WallAdjust } from "./logic/B06_Section_Cross_Culvert_Types";
import type {
ExtraWallControl,
InletStructureControl,
@@ -23,7 +23,11 @@ import type {
StationWidthControl,
StructureSpanControl,
} from "./B06_Section_UI_Cross_View";
import { culvertOwnerFor, culvertReach, wallStandsAt } from "./B06_Section_UI_Cross_Culvert_Wire";
import {
culvertOwnerFor,
culvertReach,
wallStandsAt,
} from "./logic/B06_Section_Cross_Culvert_Wire";
import { createCulvertOptionWriter } from "./B06_Section_Api_Culvert_Options";
import { createBodyControls } from "./B06_Section_UI_Page_Ford_Controls";
import {
+2 -2
View File
@@ -46,8 +46,8 @@ import { cardZoomStates } from "./B06_Section_UI_Cross_View_Zoom";
import { CROSS_GRID_GAP, CROSS_HEIGHT, createIdleQueue } from "./B06_Section_UI_Section_Common";
import { eagerCardCount, pendingCard } from "./B06_Section_UI_Section_View_Lazy";
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
import { culvertLinkFor as culvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
import { culvertLinkFor as culvertLink } from "./logic/B06_Section_Cross_Culvert_Wire";
import type { CulvertLink } from "./logic/B06_Section_Cross_Culvert_Wire";
import {
CROSS_COLUMNS_BASE,
CROSS_COLUMNS_MAX,
@@ -1,5 +1,5 @@
/* =============================================================================
* B06_Section_UI_Cross_Box_Geom.ts
* B06_Section_Cross_Box_Geom.ts
* BOX암거 구체 **기하 계산** — 그리기(`_Cross_Box.ts`)와 분리(700줄 제한).
* 백엔드 `section.box` 제원을 좌표로 옮긴다(치수 결정 금지).
*
@@ -16,18 +16,18 @@
* 부재 두께는 세월교 승계(측벽 0.2·상판 0.3·저판 0.3)이며 백엔드가 실어 보낸다.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import type { BoxSet, CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import { groundLine } from "../B06_Section_UI_Cross_Wall_Hatch";
import type { BoxSet, CrossSection, SectionSample } from "../B06_Section_Api_Fetch";
import type {
BasinLayout,
CulvertDesignTrim,
OffsetPoint,
OutletFillSegment,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
import { FILL_SLOPE_RATIO_MIN } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { designInterpolator, groundInterpolator } from "./logic/B06_Section_UI_Cross_Culvert_Solve";
import { structureCutPoints, structureCutSpec } from "./logic/B06_Section_UI_Cross_Structure_Cut";
} from "./B06_Section_Cross_Culvert_Types";
import { buildExtrasAt } from "./B06_Section_Cross_Culvert_Extra";
import { FILL_SLOPE_RATIO_MIN } from "./B06_Section_UI_Cross_Culvert_Const";
import { designInterpolator, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
import { structureCutPoints, structureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
/** 구체 한쪽 끝의 조작값 — 좌·우를 따로 잡는다(2026-08-25 사용자). */
export interface BoxSideAdjust {
@@ -0,0 +1,58 @@
/* =============================================================================
* B06_Section_Cross_Culvert.ts
* 배수관 세트 기하 입구 — `computeCulvertLayout` · 고름 키 · 관 겹 꼭짓점.
* 그리기(`UI_Cross_Culvert.ts`)는 여기서 받은 좌표를 SVG 로 옮기기만 한다.
* ========================================================================== */
import type { CulvertLayout, PipeEnd } from "./B06_Section_Cross_Culvert_Types";
export { MIN_PIPE_COVER_M, computeCulvertLayout } from "./B06_Section_Cross_Culvert_Geom";
export type {
BasinLayout,
BasinShape,
CulvertDesignTrim,
CulvertLayout,
EndFace,
InletStructureChoice,
} from "./B06_Section_Cross_Culvert_Geom";
/** 기슭막이 선택 키 — 측점 안에서 어느 벽인지 가린다. 추가 벽은 `extra{n}`. */
// "own" = 배관과 무관한 **독립 기슭막이**(구조물 정본 D군). 조정창·조작값 저장은
// 배관 벽과 같은 체계를 그대로 쓴다(2026-08-28 사용자: 배관측점처럼 이동).
export type RevetKey = "inlet" | "outlet" | "own" | `extra${number}` | `bextra${number}`;
/** 고름 전용 키 — 벽 키 + 관(118-1) · 도수로 띠(152-4) — 조정창 없음 · 강조만. 조정 함수에는 `RevetKey` 만. */
export type RevetPick = RevetKey | "pipe" | "chute";
// 미세한 틈 방지: 양 끝을 축 방향으로 아주 조금만 밀어 넣는다. 벽 전면이 1:0.3으로
// 기울어 있어 크게 밀면 관 하단이 벽 **바깥으로 삐져나온다**(2026-08-20 사용자 ③
// 위치 어긋남 원인) — 렌더링 틈만 덮을 만큼(2㎝)으로 줄였다.
const OVERLAP_M = 0.02;
/** 관 한 겹의 유입 · 유출 끝 — 축 법선으로 `delta`(m) 벌림(외경 = 관 두께 · 내경 = 0). */
export function pipeShellCorners(layout: CulvertLayout, delta: number): [PipeEnd, PipeEnd] {
const { axis, normal } = layout.pipeAxis;
const pushOut = (corner: PipeEnd, dirSign: number): PipeEnd => ({
bottom: {
offset: corner.bottom.offset + dirSign * axis.offset * OVERLAP_M,
elevation: corner.bottom.elevation + dirSign * axis.elevation * OVERLAP_M,
},
top: {
offset: corner.top.offset + dirSign * axis.offset * OVERLAP_M,
elevation: corner.top.elevation + dirSign * axis.elevation * OVERLAP_M,
},
});
const shifted = (corner: PipeEnd): PipeEnd => ({
bottom: {
offset: corner.bottom.offset - normal.offset * delta,
elevation: corner.bottom.elevation - normal.elevation * delta,
},
top: {
offset: corner.top.offset + normal.offset * delta,
elevation: corner.top.elevation + normal.elevation * delta,
},
});
return [
shifted(pushOut(layout.pipeCorners.inlet, -1)),
shifted(pushOut(layout.pipeCorners.outlet, 1)),
];
}
@@ -1,5 +1,5 @@
/* =============================================================================
* B06_Section_UI_Cross_Culvert_Extra.ts
* B06_Section_Cross_Culvert_Extra.ts
* 유출측 **성토부선 + 추가 기슭막이(다단)** — Geom에서 분리(700줄 제한).
*
* 다단 기슭막이는 **1:1.2 고정 물매에서 안정적으로 성토고를 올리는 수단**이다
@@ -23,16 +23,16 @@ import {
REVET_LEAN_RATIO,
REVET_THICKNESS_M,
WALL_MIN_PURE_HEIGHT_M,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
} from "./B06_Section_UI_Cross_Culvert_Const";
import type {
OffsetPoint,
OutletFillSegment,
WallAdjust,
WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { structureCutPoints } from "./logic/B06_Section_UI_Cross_Structure_Cut";
import type { StructureCutSpec } from "./logic/B06_Section_UI_Cross_Structure_Cut";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
} from "./B06_Section_Cross_Culvert_Types";
import { structureCutPoints } from "./B06_Section_UI_Cross_Structure_Cut";
import type { StructureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
import { buildRevetWallGeometry } from "../B06_Section_UI_Cross_Wall";
export interface OutletExtrasInput {
/** 성토부선 시작점 = 관 유출 하단 꼭짓점(배관 벽의 성토선 시작 자리). */
@@ -1,13 +1,13 @@
/* =============================================================================
* B06_Section_UI_Cross_Culvert_Geom.ts
* B06_Section_Cross_Culvert_Geom.ts
* 배수관 세트(배관·기슭막이·성토부) **기하 계산** — 그리기(`_Cross_Culvert.ts`)와 분리.
* 백엔드 `section.culvert` 제원을 좌표로 옮긴다(치수 결정 금지). 기슭막이는 전면
* 1:n 평행사변형(n = 품셈 표준경사 · `outlineLeanOf`), 관 끝단면은 구조물 변과 평행, 성토 경사선은 벽 접점에서 끊는다
* (designTrim). 유입 = 상단측. 집수정은 `_Basin.ts`, 성토부·다단은 `_Extra.ts`.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
import { groundLine } from "../B06_Section_UI_Cross_Wall_Hatch";
import type { CrossSection, CulvertSideSpec, SectionSample } from "../B06_Section_Api_Fetch";
import {
revetWallSpec,
pipeWallDefaultD,
@@ -19,10 +19,10 @@ import {
REVET_LEAN_RATIO,
REVET_THICKNESS_M,
revetTargetHeight,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { outlineLeanOf } from "./logic/B06_Section_UI_Cross_Lean";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
} from "./B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
import { buildRevetWallGeometry } from "../B06_Section_UI_Cross_Wall";
import {
bodyGuardAxes,
bodyGuardBase,
@@ -30,8 +30,8 @@ import {
bodyGuardHeight,
fordPipeFit,
mergeGuardTrim,
} from "./B06_Section_UI_Cross_Body_Guard";
import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
} from "../B06_Section_UI_Cross_Body_Guard";
import { ZERO_ADJUST } from "./B06_Section_Cross_Culvert_Types";
import type {
BasinLayout,
BasinAdjust,
@@ -42,12 +42,12 @@ import type {
OffsetPoint,
WallAdjust,
WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { DEFAULT_BASIN_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
} from "./B06_Section_Cross_Culvert_Types";
import { DEFAULT_BASIN_ADJUST } from "./B06_Section_Cross_Culvert_Types";
// 상수·자료형은 분리 파일에 있고, 기존 import 경로를 유지하기 위해 그대로 다시 내보낸다.
export * from "./logic/B06_Section_UI_Cross_Culvert_Const";
export * from "./B06_Section_UI_Cross_Culvert_Types";
export * from "./B06_Section_UI_Cross_Culvert_Const";
export * from "./B06_Section_Cross_Culvert_Types";
import {
applyBasinPipeFill,
@@ -56,16 +56,16 @@ import {
decideInletBasin,
BASIN_INNER_WIDTH_M,
inletChoiceAvailability,
} from "./logic/B06_Section_UI_Cross_Culvert_Basin";
import { floatFillStart, inletGroundConnector } from "./B06_Section_UI_Cross_Culvert_Extra";
import { withBackCut } from "./logic/B06_Section_UI_Cross_Structure_Cut";
} from "./B06_Section_UI_Cross_Culvert_Basin";
import { floatFillStart, inletGroundConnector } from "./B06_Section_Cross_Culvert_Extra";
import { withBackCut } from "./B06_Section_UI_Cross_Structure_Cut";
import {
buildCulvertTiers,
drawnFillPoints,
outletPipeEnd,
slopeZeroAnchor,
} from "./B06_Section_UI_Cross_Culvert_Tiers";
import type { FillSlopeSegment, WallVertical } from "./logic/B06_Section_UI_Cross_Culvert_Solve";
} from "./B06_Section_Cross_Culvert_Tiers";
import type { FillSlopeSegment, WallVertical } from "./B06_Section_UI_Cross_Culvert_Solve";
import {
cutLength,
fillSlopeOf,
@@ -75,7 +75,7 @@ import {
pipeAxisSolver,
placePipeWall,
slopeLengthAlong,
} from "./logic/B06_Section_UI_Cross_Culvert_Solve";
} from "./B06_Section_UI_Cross_Culvert_Solve";
/** 배수관 세트 기하 계산. 부족한 입력이면 null — 그리기와 분리(설계선 트림이 먼저 쓴다). */
export function computeCulvertLayout(
@@ -1,22 +1,22 @@
/* =============================================================================
* B06_Section_UI_Cross_Culvert_Tiers.ts
* B06_Section_Cross_Culvert_Tiers.ts
* 배수관 세트 기하(`_Cross_Culvert_Geom`)에서 떼어 낸 **벽 자리 도우미 + 다단 흙막이** 층.
* 다단 벽은 `buildExtrasAt`(공용 `buildRevetWallGeometry`)이 세우고, 여기서는 유출측과
* 집수정 계류측 두 채널의 입력을 짜고 면적용(미확정 단 제외) 결과를 함께 돌려준다.
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
import type { CrossSection } from "../B06_Section_Api_Fetch";
import {
GUARD_CUT_BERM_M,
REVET_EMBED_DEPTH_M,
REVET_FILL_TOP_M,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { OutletExtrasResult } from "./B06_Section_UI_Cross_Culvert_Extra";
import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
import { fillWallBaseWidth } from "./logic/B06_Section_UI_Cross_Culvert_Solve";
import type { OffsetPoint, WallAdjust, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import { outlineLeanOf } from "./logic/B06_Section_UI_Cross_Lean";
import { structureCutSpec } from "./logic/B06_Section_UI_Cross_Structure_Cut";
} from "./B06_Section_UI_Cross_Culvert_Const";
import type { OutletExtrasResult } from "./B06_Section_Cross_Culvert_Extra";
import { buildExtrasAt } from "./B06_Section_Cross_Culvert_Extra";
import { fillWallBaseWidth } from "./B06_Section_UI_Cross_Culvert_Solve";
import type { OffsetPoint, WallAdjust, WallLayout } from "./B06_Section_Cross_Culvert_Types";
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
import { structureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
// d의 0점 = **성토선 길이 0 지점**(벽 상단 = 노견 — 2026-08-23 사용자 확정).
// 그 자리(하단선 중점)는 이음선 상단점이 노견 끝에 오는 offset이고, 기본 자리는
@@ -0,0 +1,246 @@
/* =============================================================================
* B06_Section_Cross_Culvert_Types.ts
* 배수관 세트 기하 **자료형** — 기하 계산(`_Cross_Culvert_Geom.ts`)에서 분리했다
* (700줄 제한). 계산·그리기 양쪽이 함께 쓴다(그리기 글 = `UI_Cross_Culvert_Types`).
* ========================================================================== */
import type { CulvertSet } from "../B06_Section_Api_Fetch";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
/**
* 기슭막이 1매의 사용자 조작값(2026-08-22 확정 4축):
* x = 좌우(m, + = 계류측 바깥) — 노견(또는 윗단 시작점 수평선)을 늘려 평행이동,
* 성토선 물매 1:1.2 불변.
* d = 상하(m, 수평 성분, + = 사면 아래) — 성토선을 타고 대각 이동.
* **배관(유입·유출) 벽**은 0점 = 성토선 길이 0 지점(벽 상단 = 노견)인
* 절대값(2026-08-23 사용자 확정), null = 기본 자리(성토선 0 지점 · 79-5).
* 추가 벽은 종전대로 자동 자리 기준 상대값(null = 0).
* h = 높이(m). 배관 벽은 **순수 높이**(바닥~상단, 관 아래 0.5 포함 —
* 2026-08-23), 추가 벽은 계산용(근입 0.5 위 기준선~상단). null = 기본값
* (배관 관경+1.0·일반 1.5, 재질 한계로 잘림).
* m = 재질. null = 기본값(메쌓기). 한계: 메 2.0 / 찰 3.0 / 콘크리트 5.0.
*/
export interface WallAdjust {
x: number;
d: number | null;
h: number | null;
/** 형태(`REVET_FORMS` 문자열). 옛 저장분은 재질 코드(dry/wet/concrete) — `revetFormOf`가 옮긴다. */
m: string | null;
}
/** 조작값 기본(전부 자동). */
export const ZERO_ADJUST: WallAdjust = { x: 0, d: null, h: null, m: null };
export interface OffsetPoint {
offset: number;
elevation: number;
}
/** 점들을 감싸는 가상 사각형 — 고르기 판정면만(그림 아님) · 세월교 측벽 ㄴ(79-6) · 집수정(190-1). */
export function outlineBox(points: OffsetPoint[]): OffsetPoint[] {
const offsets = points.map((point) => point.offset);
const elevations = points.map((point) => point.elevation);
const [left, right] = [Math.max(...offsets), Math.min(...offsets)];
const [top, bottom] = [Math.max(...elevations), Math.min(...elevations)];
return [
{ offset: left, elevation: top },
{ offset: right, elevation: top },
{ offset: right, elevation: bottom },
{ offset: left, elevation: bottom },
];
}
/** 설계선(성토 경사) 트림 범위 — 이 밖은 벽·보호공이 대신하므로 그리지 않는다. */
export interface CulvertDesignTrim {
minOffset: number;
maxOffset: number;
/** 트림 경계에서 성토 경사선이 **닿아야 할 표고**(= 벽 이음선 상단점). 없으면 설계선 그대로. */
minElevation?: number;
maxElevation?: number;
/**
* 노견 → (노폭 연장) → 벽 이음선 상단점 폴리라인. 물매가 1:1.2 고정이라 벽이 밖으로
* 나간 만큼 **노폭이 늘어난다** — 그 연장 구간이 가운데 점으로 들어온다.
*/
minSlope?: { points: OffsetPoint[] };
maxSlope?: { points: OffsetPoint[] };
}
export interface WallLayout {
role: "inlet" | "outlet" | "extra";
/** 추가 기슭막이 번호(role === "extra"일 때만) — 조정창 키 `extra{n}`이 된다. */
extraIndex?: number;
form: string | null;
lengthM: number | null;
/** 배면(도로측) 바닥 offset. */
backOffset: number;
/** 전면(계류측) 바닥 offset. */
outerOffset: number;
base: number;
/** 벽 높이(m) = **계산용**(관 invert~상단, 근입 제외 — 2026-08-22 사용자 확정).
* 기초는 시공 시 묻히는 부분이라 높이 표기·한계 검사(찰 3.0/메 2.0) 모두 이 기준. */
height: number;
/**
* 바닥이 원지반에서 뜬 간격(m, 2026-08-22 사용자 확정). 손으로 안쪽에 당겨 높이
* 한계로도 사면선에 못 닿을 때 바닥을 띄워 최소 노견을 확보한다 — 뜬 공간은
* 별도 지지 구조물로 메운다(추가 예정). 0이면 지반 위(근입 0.5m).
*/
floatGapM: number;
/** 좌우 이동의 **하한**(m, 2026-09-07). 선반 길이가 음수가 되지 않게 x 는 최소
* `1.2 × 상하 내림`까지 따라 나간다. 요청이 이 값보다 작으면 눌러도 벽이 안 움직임 —
* 그 까닭을 툴팁으로 알린다. */
shiftFloorM?: number;
/** 재질(메/찰/콘크리트) — 높이 한계를 정한다(2026-08-22 사용자). */
material: RevetMaterial;
/** 전면 기울기 1:n 의 n — 품셈 13-4-4 [주]⑪ 표준경사(`wallLeanRatio`, B08 과 한 벌 · 2026-09-14). */
lean: number;
outward: number;
/** 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점 — 데이터 좌표. */
points: OffsetPoint[];
/** 상단 배면 꼭짓점 — 성토 사면선과의 접점(설계선 트림 경계). */
topBack: OffsetPoint;
/** 이음선 상단점(평행사변형↔사다리꼴 상단 교차점) — 안 무늬 이음선 자리. */
topJoint: OffsetPoint;
/** 성토 위 이음 — 윗면 사면 쪽 끝에서 0.1 m 안(371-1). 성토 사면선이 여기서 끝난다. */
fillTop: OffsetPoint;
/** 하단선 양 끝 — **수평 기초**(2026-08-22 사용자 확정): 발자국 최저 지반(또는 관
* invert) − 0.5m 표고의 수평선. 전면 발끝은 1:0.3 경사선을 하단까지 연장한 자리다. */
bottomBack: OffsetPoint;
bottomFront: OffsetPoint;
/** 옹벽(구조물 `retaining_wall`) — 안 무늬에 물빼기 파이프 단면을 더함(73-2). */
retaining?: boolean;
}
/**
* 유입 집수정 단면 — 실무 도면(2026-08-20 사용자 제공 L형 확대본) 기준.
* · **I형** = 기슭막이와 같은 **평행사변형 벽 1매**만.
* · **ㄴ(L)형** = I형 + **바닥판**.
* · **ㄷ형** = ㄴ형 + I형 **반대측 막음벽**.
* 돌 표현은 재질 확정 시 반영 예정(성토부 기슭막이와 동일 규칙) — 지금은 윤곽만.
*/
export type BasinShape = "I" | "L" | "U";
export interface BasinLayout {
shape: BasinShape;
/** 부재별 폴리곤(벽·바닥·막음벽). 각각 분리 도형으로 그린다. */
parts: Array<{ kind: "wall" | "floor"; points: OffsetPoint[] }>;
label: OffsetPoint;
/** 자동 전환 사유 — 툴팁·라벨에 쓴다(2026-08-22 재정의: 성토사면 ≤3m 기준).
* "cut" = 절토측 유입(성토사면 없음), "short" = 성토사면 3m 이하,
* "manual" = 규칙상 기슭막이 자리지만 사용자가 집수정을 선택. */
reason: "cut" | "short" | "manual";
/** 집수정이 원지반 안으로 들어갈 때의 절토선 점열(구조물 바깥 끝 → 복합경사 → 지반 교차점 · 91-4). */
cutLine: OffsetPoint[] | null;
/** 구조물이 원지반 밖으로 나올 때의 성토(되메움)선 — 경사 5도(임시), 지반 교차점까지. */
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;
direction: { offset: number; elevation: number };
}
/** 관 한쪽 끝단의 두 꼭짓점(마감면에서 자른 결과). */
export interface PipeEnd {
bottom: OffsetPoint;
top: OffsetPoint;
}
/**
* 유출측 **성토부선** 한 구간(2026-08-22 사용자 — 보호공 삭제, 윗면 선만 성토부선으로).
* 관 하단 꼭짓점(또는 앞 추가 벽 전면 상단)에서 1:1.2로 원지반(또는 다음 벽)까지.
*/
export interface OutletFillSegment {
points: OffsetPoint[];
lengthM: number;
/** 구간 물매(1:n) — 벽 사이 구간은 높이 역산으로 정확히 1.2다. */
ratio?: number;
/** 선 성격(2026-08-22 사용자 ②③): fill = 성토선(기본), cut = 0도/접속 절토선. */
kind?: "fill" | "cut";
/** 사면길이 5m 이상 — 기슭막이 의무 구간(별표2·성토_비탈면 §2). */
overLimit: boolean;
}
export interface CulvertLayout {
culvert: CulvertSet;
/** 원지반 — 도수로 · 돌붙임 길이가 사면보다 길면 이어 그림(294-4). */
ground?: OffsetPoint[];
pipe: { inlet: OffsetPoint; outlet: OffsetPoint; lengthM: number; slopePct: number };
/** 관 축·법선 단위벡터(그리기가 두께 방향으로 밀 때 쓴다). */
pipeAxis: { axis: OffsetPoint; normal: OffsetPoint };
/** 관 양 끝단 꼭짓점 — 보호공 시작점이 유출 하단 꼭짓점을 그대로 쓴다. */
pipeCorners: { inlet: PipeEnd; outlet: PipeEnd };
walls: WallLayout[];
/** 유출측 성토사면 — 경사길이(m)·법정 5m 충족 여부(별표2)·풀어낸 물매(1:n). */
fillSlope: {
lengthM: number;
withinLimit: boolean;
/** 사면선이 벽 이음선 상단점을 지나도록 역산한 물매. 범위 밖이면 끝값으로 고정. */
ratio: number;
/** 물매가 1.2~2.0 범위를 벗어났는지 — 도면 경고용. */
ratioClamped: boolean;
/** 물매 1:1.2를 지키려고 늘린 **노폭 연장량**(m, 편측). 0이면 연장 없음. */
roadWideningM: number;
/**
* 기슭막이만으로는 성토를 받을 수 없는 자리(성토고 3m↑인데 사면 5m·물매 1:1.2를
* 동시에 만족하는 벽 자리가 없다) — **옹벽·석축 등 구조물 검토 대상**
* (`성토_비탈면.md` §2 "5m 초과 시 구조물 설치 의무").
*/
structureRequired: boolean;
};
/** 유출측 성토부선(보호공 대체 — 2026-08-22). 끝 구간이 5m 이상이면 추가 가능. */
outletFill: { segments: OutletFillSegment[]; addable: boolean };
/** 유입 집수정(ㄴ·ㄷ형) 계류측 성토부선 — 유출측과 같은 체계(2026-08-22 사용자). */
basinFill: { segments: OutletFillSegment[]; addable: boolean };
/** 집수정 계류측 다단 기슭막이 — 조정창 키 `bextra{n}`. */
basinExtras: WallLayout[];
/** 한계에 잘린 뒤의 **실제 적용된** 집수정 조작값 — 조정창이 되받는다. */
basinAdjust: BasinAdjust;
/** 유입 기슭막이의 관 시작 접속선(2026-08-22 ②) — 위면 0도 성토선(지반 정지),
* 아래면 0도 1m 후 표준 절토 경사선. 집수정·접속 불필요면 null. */
inletFill: OutletFillSegment | null;
/** 추가 기슭막이(배관 없는 벽 — 계단식). 조정창 키 = `extra{n}`. */
extraWalls: WallLayout[];
/** 유입 집수정 단면(있을 때만). */
basin: BasinLayout | null;
designTrim: CulvertDesignTrim | null;
/**
* **실제 적용된** 기슭막이 이동량(m, + = 계류측 바깥). 요청값이 노견·사면 역전·사면 끝
* 한계에 걸리면 잘려 들어온다 — 조정창은 이 값을 보여 주고 저장한다. 그러지 않으면
* 눌러도 안 움직이는데 숫자만 계속 커진다(2026-08-21 사용자 지적).
*/
revetShift: {
inlet: WallAdjust;
outlet: WallAdjust;
extras: WallAdjust[];
/** 집수정 계류측 다단의 적용 조작값. */
basinExtras: WallAdjust[];
};
/**
* 조정창 드롭다운 선택지 가용성(2026-08-22 사용자 — 상황에 안 맞는 선택지 숨김):
* revetAllowed = 기슭막이+배관(배관이 원지반에 안 묻힐 때만),
* basinLUAllowed = 집수정 ㄴ·ㄷ형(구조물이 원지반으로 밀고 들어갈 때만).
*/
inletOptions: { revetAllowed: boolean; basinLUAllowed: boolean };
/** 세월교 · 박스 흙막이(73-4) — 본체 끝 자리의 배관 4축 x(적용 x 의 0점). 본체 없으면 없음. */
bodyX?: { inlet: number; outlet: number } | null;
/** 유입 · 유출 벽의 순수 높이(m) — 높이 빈 칸인 연동 옆 카드가 물려받음(152-2). */
pureHeights?: { inlet: number; outlet: number };
}
/** 유입측 구조물 사용자 선택 — auto = 규칙 기반 자동 선택. */
export type InletStructureChoice = "auto" | "revet" | "I" | "L" | "U";
@@ -1,27 +1,27 @@
/* =============================================================================
* B06_Section_UI_Cross_Culvert_Wire.ts
* B06_Section_Cross_Culvert_Wire.ts
* 횡단 카드의 배수관 세트 **계산 호출 + 이동량 되받기(토스트)** — Cross_View에서
* 분리(700줄 제한). 카드가 다시 그려질 때마다 한 번 불린다.
* ========================================================================== */
import { bodyAbsolute } from "./B06_Section_UI_Cross_Body_Guard";
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import { computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert";
import { bodyAbsolute } from "../B06_Section_UI_Cross_Body_Guard";
import type { CrossSection, SectionSample } from "../B06_Section_Api_Fetch";
import { computeCulvertLayout } from "./B06_Section_Cross_Culvert";
import type {
CulvertLayout,
InletStructureChoice,
RevetKey,
RevetPick,
} from "./B06_Section_UI_Cross_Culvert";
import type { BasinAdjust, CulvertDesignTrim } from "./B06_Section_UI_Cross_Culvert_Types";
} from "./B06_Section_Cross_Culvert";
import type { BasinAdjust, CulvertDesignTrim } from "./B06_Section_Cross_Culvert_Types";
import {
basinSpanOfSpec,
FILL_SLOPE_RATIO_MIN,
revetSpanOfSpec,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { EXTRA_SPAN_DEFAULT } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { StructureSpan } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
} 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_Cross_Culvert_Types";
import type { LocaleKey } from "@ui/ui_template_locale";
/**
* 기슭막이 4축 조작 제어(2026-08-22 확정) — 벽을 눌러 고르고 조정창에서
@@ -1,5 +1,5 @@
/* =============================================================================
* B06_Section_UI_Cross_Ford_Geom.ts
* B06_Section_Cross_Ford_Geom.ts
* 세월교 구체 **기하 계산** — 그리기(`_Cross_Ford.ts`)와 분리(700줄 제한).
* 백엔드 `section.ford` 제원을 좌표로 옮긴다(치수 결정 금지).
*
@@ -19,19 +19,19 @@
* 성토측이라도 기슭막이 세트(다단·재질)와는 연계하지 않는다.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import type { CrossSection, FordSet, SectionSample } from "./B06_Section_Api_Fetch";
import { GUARD_CUT_BERM_M, pipeWallThicknessM } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { groundLine } from "../B06_Section_UI_Cross_Wall_Hatch";
import type { CrossSection, FordSet, SectionSample } from "../B06_Section_Api_Fetch";
import { GUARD_CUT_BERM_M, pipeWallThicknessM } from "./B06_Section_UI_Cross_Culvert_Const";
import type {
BasinLayout,
CulvertDesignTrim,
OffsetPoint,
OutletFillSegment,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { basinApproachPoints, buildBasin } from "./logic/B06_Section_UI_Cross_Culvert_Basin";
import { bermCut, buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
import { designInterpolator, groundInterpolator } from "./logic/B06_Section_UI_Cross_Culvert_Solve";
import { structureCutSpec } from "./logic/B06_Section_UI_Cross_Structure_Cut";
} from "./B06_Section_Cross_Culvert_Types";
import { basinApproachPoints, buildBasin } from "./B06_Section_UI_Cross_Culvert_Basin";
import { bermCut, buildExtrasAt } from "./B06_Section_Cross_Culvert_Extra";
import { designInterpolator, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
import { structureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
/** 측벽 한 매의 조작값 — 좌우만(139-3). `heightM` · `slopeM` 은 옛 저장(측벽마다 높이 · 1:1.2
* 대각) 읽기 전용 — `dropM` 이 없을 때 바닥판 표고로 옮김(`fordDropOf`). */
@@ -0,0 +1,118 @@
/* =============================================================================
* B06_Section_Cross_Ford_Pavement.ts
* 물넘이포장 기하 — 유입 쪽 · 노면 양 끝 · 월류 높이선 · 유출측 성토사면 선(287-2 · 294 · 315).
* 그리기는 `UI_Cross_Ford_Pavement.ts` · 3D 띠(B05)도 같은 선을 씀.
* ========================================================================== */
import type { CrossDesign } from "../B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_Cross_Culvert_Types";
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
export interface FordPavementSpec {
/** 관 지점 누가거리 — 폭 절반 걸친 옆 카드도 같은 값. */
chainage_m?: number;
span_m: number;
/** 월류 높이(m) — 노면 위 점선. 없으면 점선을 안 그림(수치를 지어내지 않는다). */
depth_m: number | null;
/** 노면 경사(%) — 경사각도 환산 · 서버 설계 노면이 이 값으로 기욺. */
slope_pct: number | null;
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
defaulted?: string[];
/** 도수로 · 돌붙임(`ford_chute_{n}_*` · 136-2) — 유출측 성토사면 띠 · 길이 = 사면 따라(286-3 · 비면 원지반까지). */
chute?: { structure: string; thickness_m: number; length_m?: number };
}
/** 유입(상류)이 좌측(+offset)인가 — 기울인 노면이면 높은 노면 끝 · 아니면 설계선 양 끝 표고가 높은
* 쪽(286-2) · 같으면 단면유형. */
export function inflowLeft(design: CrossDesign): boolean {
const edges = design.road_edges;
if (design.tilted && edges && Math.abs(edges.left.elevation_m - edges.right.elevation_m) > 1e-6)
return edges.left.elevation_m > edges.right.elevation_m;
const line = [...(design.design_line ?? [])].sort((a, b) => a.offset_m - b.offset_m);
const gap = line.length >= 2 ? line[line.length - 1].elevation_m - line[0].elevation_m : 0;
return Math.abs(gap) > 1e-3 ? gap > 0 : design.section_mode !== "right_cut";
}
/** 콘크리트 · 노면 양 끝 — 낮은 쪽을 늘였으면(294-3 `tilt_reach`) 그 쪽은 원지반과 만난 자리까지. */
export function fordEdges(design: CrossDesign): CrossDesign["road_edges"] | undefined {
const edges = design.road_edges;
const reach = design.tilt_reach;
if (!edges || !reach) return edges;
const end = { offset_m: reach.offset_m, elevation_m: reach.elevation_m };
return reach.side === "left" ? { ...edges, left: end } : { ...edges, right: end };
}
/**
* 월류 높이선(315-2) — 노면 양 끝 위 월류 높이 · 노면과 같은 각도. 성토 쪽(노면 끝 밖 설계선이 내려감)은
* 성토 사면선을 노면 끝 너머 위 · 안쪽으로 늘여 월류 높이선과 만나는 자리에서 끝 → 노면 끝까지 그 연장선.
* 절토 쪽 · 늘인 노면 끝(294-3)은 노면 끝 위 그대로. 좌(+offset) → 우 차례.
*/
export function overflowLine(design: CrossDesign, depth: number): OffsetPoint[] {
const edges = fordEdges(design);
if (!edges) return [];
const { left, right } = edges;
const span = Math.abs(left.offset_m - right.offset_m);
const side = (edge: typeof left, other: typeof left): OffsetPoint[] => {
const top = { offset: edge.offset_m, elevation: edge.elevation_m + depth };
const outward = Math.sign(edge.offset_m - other.offset_m) || 1;
const next = (design.design_line ?? [])
.filter((p) => (p.offset_m - edge.offset_m) * outward > 1e-6)
.sort((a, b) => (a.offset_m - b.offset_m) * outward)[0];
const cut = edge.offset_m === design.tilt_reach?.offset_m; // 늘인 끝 = 원지반(294-3 · 절토)
if (cut || !next || next.elevation_m >= edge.elevation_m - 1e-6 || span < 1e-9) return [top];
// 안쪽 d m 에서 성토선 = edge + fill·d · 월류선 = edge + depth + road·d → d = depth / (fill − road)
const fill = (edge.elevation_m - next.elevation_m) / Math.abs(next.offset_m - edge.offset_m);
const road = (other.elevation_m - edge.elevation_m) / span;
if (fill <= road + 1e-9) return [top];
const d = Math.min(depth / (fill - road), span);
const meet = { offset: edge.offset_m - outward * d, elevation: edge.elevation_m + fill * d };
return [{ offset: edge.offset_m, elevation: edge.elevation_m }, meet];
};
return [...side(left, right), ...side(right, left).reverse()];
}
/** 원지반 꺾은선의 그 오프셋 표고(밖은 끝값). */
function groundZ(ground: readonly OffsetPoint[], offset: number): number | null {
if (!ground.length) return null;
if (offset <= ground[0].offset) return ground[0].elevation;
for (let i = 1; i < ground.length; i += 1) {
const [a, b] = [ground[i - 1], ground[i]];
if (offset > b.offset) continue;
const span = b.offset - a.offset;
return span > 1e-9
? a.elevation + ((b.elevation - a.elevation) * (offset - a.offset)) / span
: b.elevation;
}
return ground[ground.length - 1].elevation;
}
/**
* 유출측(낮은 쪽) 성토사면 선(136-2 · 294-4) — 낮은 쪽 끝단(늘인 노면 끝 · 아니면 유출 쪽 노견 끝)에서
* 설계선을 바깥으로 따라 내려가는 동안만(오르면 절토라 멈춤) · 원지반(`ground`)에 닿으면 끝.
* 성토가 없으면(끝단이 이미 원지반 · 양측 절토 물넘이) 점 하나 = 띠 없음.
*/
export function outletFillLine(
design: CrossDesign,
ground?: readonly OffsetPoint[],
): OffsetPoint[] {
const edges = fordEdges(design);
if (!edges || !design.design_line?.length) return [];
const reach = design.tilt_reach;
const outLeft = reach ? reach.side === "left" : !inflowLeft(design); // +offset = 좌측
const outward = outLeft ? 1 : -1;
const edge = outLeft
? Math.max(edges.left.offset_m, edges.right.offset_m)
: Math.min(edges.left.offset_m, edges.right.offset_m);
const points = design.design_line
.filter((p) => (p.offset_m - edge) * outward >= -1e-6)
.sort((a, b) => (a.offset_m - b.offset_m) * outward);
const line: OffsetPoint[] = [];
for (const p of points) {
const last = line[line.length - 1];
if (last && p.elevation_m > last.elevation + 1e-6) break;
line.push({ offset: p.offset_m, elevation: p.elevation_m });
const z = ground ? groundZ(ground, p.offset_m) : null;
if (z !== null && Math.abs(z - p.elevation_m) < 1e-3) break; // 원지반에 닿음 = 끝
}
return line;
}
@@ -33,7 +33,7 @@ import type {
EndFace,
OffsetPoint,
InletStructureChoice,
} from "../B06_Section_UI_Cross_Culvert_Types";
} from "./B06_Section_Cross_Culvert_Types";
import { structureCutPoints, structureCutSpec } from "./B06_Section_UI_Cross_Structure_Cut";
/** 자동(기본) 집수정 모양(54-36) — 좌측 폼 「(기본)」 표시도 이 값. */
@@ -5,7 +5,7 @@
* ========================================================================== */
import type { CrossSection, CulvertSideSpec } from "../B06_Section_Api_Fetch";
import type { SpanRole, SpanValues } from "../B06_Section_UI_Cross_Culvert_Wire";
import type { SpanRole, SpanValues } from "./B06_Section_Cross_Culvert_Wire";
/** 관 위 최소 토피(m) — 프론트 정의처(백엔드는 `B06_Section_Engine_Culvert.MIN_PIPE_COVER_M`).
* ⚠ 임도 배수관 토피 규정이 지식DB에 없어 별표2 교량·암거 "복토 50㎝ 이상"을 교차
@@ -13,12 +13,7 @@ import {
REVET_THICKNESS_M,
REVET_FILL_TOP_M,
} from "./B06_Section_UI_Cross_Culvert_Const";
import type {
EndFace,
OffsetPoint,
PipeEnd,
WallAdjust,
} from "../B06_Section_UI_Cross_Culvert_Types";
import type { EndFace, OffsetPoint, PipeEnd, WallAdjust } from "./B06_Section_Cross_Culvert_Types";
/** 기슭막이 하단선 폭(m) — 사다리꼴 밑변. 자리 기준(하단선 중점) 환산에 쓴다.
* `lean` = 그 벽의 전면 기울기(표준경사 · `revetWallSpec().lean`). */
@@ -11,7 +11,7 @@
import { cutProfilePoints } from "../../common_util/common_util_cross_berm";
import type { BermSpec } from "../../common_util/common_util_cross_berm";
import type { CrossDesign } from "../B06_Section_Api_Fetch";
import type { OffsetPoint } from "../B06_Section_UI_Cross_Culvert_Types";
import type { OffsetPoint } from "./B06_Section_Cross_Culvert_Types";
/** 절토 제원 — 암(또는 단일) 경사 · 토사 경사 · 암 경계 오프셋(없으면 경사 하나) · 소단. */
export interface StructureCutSpec {
@@ -8,10 +8,10 @@
* ========================================================================== */
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";
import type { InletStructureChoice } from "../B06_Section_UI_Cross_Culvert";
import type { BasinAdjust } from "./B06_Section_Cross_Culvert_Types";
import type { FordAdjust } from "./B06_Section_Cross_Ford_Geom";
import type { BoxAdjust } from "./B06_Section_Cross_Box_Geom";
import type { InletStructureChoice } from "./B06_Section_Cross_Culvert";
export interface CrossPatchSources {
/** 암 경계선 오프셋(누가거리 문자열 키). */
+1 -1
View File
@@ -122,7 +122,7 @@ const resolve = (base) =>
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
const { wallStandsAt } = loadTs(B06("B06_Section_UI_Cross_Culvert_Wire"));
const { wallStandsAt } = loadTs(B06("logic/B06_Section_Cross_Culvert_Wire"));
const { applyStructurePick } = loadTs(B06("B06_Section_UI_Page_Structure_Pick"));
const fixture = JSON.parse(
@@ -33,9 +33,9 @@ const { buildCorridorStructures } = loadTs(B05("B05_Profile_UI_Corridor_Structur
const { structureHashParts } = loadTs(B05("logic/B05_Profile_UI_Corridor_Structures_Hash"));
const { structureTopLoops } = loadTs(B05("logic/B05_Profile_UI_Corridor_Tin_Design"));
const { chuteBandOutline, isChuteSegment } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom"));
const { computeBoxLayout } = loadTs(B06("logic/B06_Section_Cross_Box_Geom"));
const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford"));
const { outletFillLine } = loadTs(B06("B06_Section_UI_Cross_Ford_Pavement"));
const { outletFillLine } = loadTs(B06("logic/B06_Section_Cross_Ford_Pavement"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
@@ -27,7 +27,9 @@ const resolve = (base) =>
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { freezeLinkedWalls } = loadTs(B06("B06_Section_UI_Page_Link_Session.ts"));
const { computeCardCulvert, culvertLinkFor } = loadTs(B06("B06_Section_UI_Cross_Culvert_Wire.ts"));
const { computeCardCulvert, culvertLinkFor } = loadTs(
B06("logic/B06_Section_Cross_Culvert_Wire.ts"),
);
const sections = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
+1 -1
View File
@@ -19,7 +19,7 @@ const { loadTs } = makeLoader({
});
const { computeFordLayout, fordAdjustOf } = loadTs(
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Geom.ts"),
path.join(ROOT, "B06_Section", "logic/B06_Section_Cross_Ford_Geom.ts"),
);
const sections = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
+1 -1
View File
@@ -30,7 +30,7 @@ const { computeCrossDesign } = loadTs(
path.join(ROOT, "common_util", "common_util_cross_design.ts"),
);
const { computeCulvertLayout, REVET_EMBED_DEPTH_M } = loadTs(
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert_Geom.ts"),
path.join(ROOT, "B06_Section", "logic/B06_Section_Cross_Culvert_Geom.ts"),
);
const { cutLength } = loadTs(
path.join(ROOT, "B06_Section", "logic", "B06_Section_UI_Cross_Culvert_Solve.ts"),
@@ -31,7 +31,7 @@ const resolve = (base) =>
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { computeStoredLayouts, structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts"));
const { computeCulvertLayout } = loadTs(B06("logic/B06_Section_Cross_Culvert_Geom.ts"));
const read = (name) =>
JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", name), "utf8")).cross_sections;
const clone = (v) => JSON.parse(JSON.stringify(v));
+1 -1
View File
@@ -82,7 +82,7 @@ const resolve = (base) =>
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
const { outlineBox } = loadTs(B06("B06_Section_UI_Cross_Culvert_Types"));
const { outlineBox } = loadTs(B06("logic/B06_Section_Cross_Culvert_Types"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b07_cross_overlay_sections.json"), "utf8"),
+1 -1
View File
@@ -62,7 +62,7 @@ const { loadTs } = makeLoader({
});
const B06 = (name) => loadTs(path.join(ROOT, "B06_Section", `${name}.ts`));
const { computeCulvertLayout } = B06("B06_Section_UI_Cross_Culvert_Geom");
const { computeCulvertLayout } = B06("logic/B06_Section_Cross_Culvert_Geom");
const { cutLength } = B06("logic/B06_Section_UI_Cross_Culvert_Solve");
const { pipeLengthText, pipeLengthWhole } = B06("logic/B06_Section_UI_Cross_Culvert_Const");
const { appendPipeInfoText } = B06("B06_Section_UI_Cross_Culvert_Types");
@@ -115,9 +115,9 @@ const STUBS = {
"B06_Section_Cross_Design_Session.ts": anyModule(),
"common_util_cross_design.ts": anyModule(),
"B06_Section_Structure_Layouts.ts": anyModule({ computeStoredLayouts: () => null }),
"B06_Section_UI_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_UI_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_UI_Cross_Box_Geom.ts": anyModule(),
"B06_Section_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_Cross_Box_Geom.ts": anyModule(),
"B06_Section_UI_Cross_View.ts": anyModule(),
"B06_Section_UI_Section_Common.ts": anyModule({ designElevationAt: () => undefined }),
"B06_Section_Display_Width.ts": anyModule({ displayHalfWidth: () => 10 }),
@@ -29,6 +29,9 @@ const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pa
{ "./logic/B06_Section_UI_Cross_Culvert_Const": {} },
),
"./B06_Section_UI_Cross_Design_Surface": { appendPavementOverlay() {} },
"./logic/B06_Section_Cross_Ford_Pavement": loadTs(
path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Ford_Pavement.ts"),
),
// 관 신원표(323-5 · 472-2 에서 common_util 로) — 진짜 · 장부 안 꽂음 = 옛 자리 이름
"../common_util/common_util_pick_tag": loadTs(
path.join(ROOT, "common_util", "common_util_pick_tag.ts"),
@@ -73,6 +73,7 @@ class Node {
}
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
const paveGeo = load(path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Ford_Pavement.ts"));
// 서버가 지대 낮은 쪽(우)으로 5 % 기울인 노면 · 노견 끝 ±3 · 차도 끝 ±2.5
const tilted = {
section_mode: "left_cut",
@@ -244,7 +245,7 @@ const out = {
drew,
pavePoints: ofClass("b06-chart__ford-pavement").map((k) => k.attrs.points),
planPoints: ofClass("b06-chart__ford-deck-plan").map((k) => k.attrs.points),
inflowLeft: pave.inflowLeft(tilted),
inflowLeft: paveGeo.inflowLeft(tilted),
// 287-3 월류 폭 → 높이 = 세월교와 같은 풀이(유량 1.2 ㎥/s · 폭 10 m) · 유량 없으면 null(손 입력)
heightByWidth: calc.fordHeightM(1.2, 10),
heightNoFlow: calc.fordHeightM(null, 10),
@@ -44,6 +44,7 @@ globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
const paveGeo = load(path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Ford_Pavement.ts"));
const design = input.design; // 서버가 낸 양측 절토 · 낮은 쪽 연장 설계
const svg = new Node("g");
pave.appendFordPavementOverlay(
@@ -70,18 +71,18 @@ const fordPave = load(
);
const out = {
edges: pave.fordEdges(design),
edges: paveGeo.fordEdges(design),
pavePoints: paveLayer?.attrs.points ?? null,
runNone: pts(hatch.chuteRun(slope)),
runShort: pts(hatch.chuteRun(slope, 2)),
runLong: pts(hatch.chuteRun(slope, 7, flat)),
runPointOnly: pts(hatch.chuteRun([{ points: [P(3, -4)] }])),
runPointLength: pts(hatch.chuteRun([{ points: [P(3, -4)] }], 2, flat)),
dipLine: pave.outletFillLine(design, dipGround),
dipLine: paveGeo.outletFillLine(design, dipGround),
dipChute: pave.fordChuteLine(design, {}, dipGround),
dipChuteLength: pave.fordChuteLine(design, { length_m: 1.5 }, dipGround),
fillLine: pave.outletFillLine(input.fillDesign, fillGround),
fillLineOld: pave.outletFillLine(input.fillDesign),
fillLine: paveGeo.outletFillLine(input.fillDesign, fillGround),
fillLineOld: paveGeo.outletFillLine(input.fillDesign),
fillChuteLength: pave.fordChuteLine(input.fillDesign, { length_m: 1 }, fillGround),
chuteItems: guard.CHUTE_ITEMS.map((i) => i.id),
fordPaveChuteSame:
@@ -44,6 +44,7 @@ globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
const paveGeo = load(path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Ford_Pavement.ts"));
const hatch = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"));
const fillGround = hatch.groundLine(input.fillSamples);
const dipGround = hatch.groundLine(input.dipSamples);
@@ -73,10 +74,10 @@ const out = {
fillLong: chute(input.fillDesign, fillGround, 6),
dipNone: chute(input.dipDesign, dipGround),
dipLong: chute(input.dipDesign, dipGround, 2),
fillOverflow: pave.overflowLine(input.fillDesign, 0.3),
dipOverflow: pave.overflowLine(input.dipDesign, 0.3),
fillEdges: pave.fordEdges(input.fillDesign),
dipEdges: pave.fordEdges(input.dipDesign),
fillOverflow: paveGeo.overflowLine(input.fillDesign, 0.3),
dipOverflow: paveGeo.overflowLine(input.dipDesign, 0.3),
fillEdges: paveGeo.fordEdges(input.fillDesign),
dipEdges: paveGeo.fordEdges(input.dipDesign),
deckPoints: deck?.attrs.points ?? null,
bandPoints: band?.attrs.points ?? null,
};
+1 -1
View File
@@ -62,7 +62,7 @@ const { loadTs } = makeLoader({
});
const { computeCulvertLayout } = loadTs(
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert_Geom.ts"),
path.join(ROOT, "B06_Section", "logic/B06_Section_Cross_Culvert_Geom.ts"),
);
const { appendCulvertOverlay } = loadTs(
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert.ts"),
@@ -46,7 +46,7 @@ const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { structureAreaRows, computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { layPipeOptions } = loadTs(B06("B06_Section_Section_Adjusts"));
const { applyPipeOptionsToCache } = loadTs(B06("B06_Section_UI_Page_Pipe_Options"));
const { ZERO_ADJUST } = loadTs(B06("B06_Section_UI_Cross_Culvert_Types"));
const { ZERO_ADJUST } = loadTs(B06("logic/B06_Section_Cross_Culvert_Types"));
const fresh = () =>
JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", "b06_186_1_pipe_292.json"), "utf8"))
@@ -87,9 +87,9 @@ const STUBS = {
"B06_Section_UI_Standard_Panel.ts": anyModule(),
"B06_Section_Cross_Design_Session.ts": anyModule(),
"common_util_cross_design.ts": anyModule(),
"B06_Section_UI_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_UI_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_UI_Cross_Box_Geom.ts": anyModule(),
"B06_Section_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_Cross_Box_Geom.ts": anyModule(),
"B06_Section_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
"B05_Profile_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
"config_frontend.ts": anyModule({ PIPE_TYPES: [] }),
+3 -3
View File
@@ -82,9 +82,9 @@ const STUBS = {
"B06_Section_Cross_Design_Session.ts": anyModule(),
"common_util_cross_design.ts": anyModule(),
"B06_Section_Structure_Layouts.ts": anyModule({ computeStoredLayouts: () => null }),
"B06_Section_UI_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_UI_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_UI_Cross_Box_Geom.ts": anyModule(),
"B06_Section_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_Cross_Box_Geom.ts": anyModule(),
"B06_Section_UI_Cross_View.ts": anyModule(),
"B06_Section_UI_Section_Common.ts": anyModule({ designElevationAt: () => undefined }),
"B06_Section_Display_Width.ts": anyModule({ displayHalfWidth: () => 10 }),
+1 -1
View File
@@ -71,7 +71,7 @@ const load = makeLoader(
B06("B06_Section_UI_Page_Station_Controls"),
B06("B06_Section_UI_Page_Link_Session"),
B06("B06_Section_Session_Cells"), // 조정창 칸 창구(472-5 5-D · 메모리 맵 없음)
B06("B06_Section_UI_Cross_Culvert_Types"),
B06("logic/B06_Section_Cross_Culvert_Types"),
B06("logic/B06_Section_Station_Swap"),
B06("logic/B06_Section_UI_Page_Patches"),
B06("logic/B06_Section_Design_Keys"), // 칸 목록 · 얹기 · patch 꼴 한 벌(472-6)
+3 -3
View File
@@ -71,9 +71,9 @@ const STUBS = {
"B06_Section_Cross_Design_Session.ts": anyModule({ crossDesignChoices: () => new Map() }),
"B06_Section_Section_Adjusts.ts": anyModule({ withPipeOptions: async () => undefined }),
"common_util_cross_design.ts": anyModule(),
"B06_Section_UI_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_UI_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_UI_Cross_Box_Geom.ts": anyModule(),
"B06_Section_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_Cross_Box_Geom.ts": anyModule(),
"B06_Section_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
"B05_Profile_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
"config_frontend.ts": anyModule({ PIPE_TYPES: [] }),
@@ -69,9 +69,9 @@ const STUBS = {
"B06_Section_Cross_Design_Session.ts": anyModule({ crossDesignChoices: () => new Map() }),
"B06_Section_Section_Adjusts.ts": anyModule({ withPipeOptions: async () => undefined }),
"common_util_cross_design.ts": anyModule(),
"B06_Section_UI_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_UI_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_UI_Cross_Box_Geom.ts": anyModule(),
"B06_Section_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_Cross_Box_Geom.ts": anyModule(),
"B06_Section_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
"B05_Profile_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
"config_frontend.ts": anyModule({ PIPE_TYPES: [] }),
+2 -2
View File
@@ -31,8 +31,8 @@ const resolve = (base) =>
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { computeStoredLayouts, structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
const { bodyGuardBase } = loadTs(B06("B06_Section_UI_Cross_Body_Guard.ts"));
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts"));
const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts"));
const { computeBoxLayout } = loadTs(B06("logic/B06_Section_Cross_Box_Geom.ts"));
const { computeCulvertLayout } = loadTs(B06("logic/B06_Section_Cross_Culvert_Geom.ts"));
const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment.ts"));
const fixture = JSON.parse(
+3 -3
View File
@@ -80,7 +80,7 @@ out.fordHandBedDown = fordFloor((s) => {
shiftGround(s, -2, -2, 2);
});
// ② 91-3 다단 자동 자리 — 관 180 유출 흙막이 + 2단 · 원지반을 옮겨도 단 자리 같음 · 사면 1.0 m
const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts"));
const { computeCulvertLayout } = loadTs(B06("logic/B06_Section_Cross_Culvert_Geom.ts"));
function tiers(dz) {
const s = clone(at(180));
shiftGround(s, dz);
@@ -139,8 +139,8 @@ function guardTails(base, edit = () => undefined) {
}
out.guardTails = [...guardTails(at(352.14)), ...guardTails(FORD)];
// 박스 끝 · 세월교 측벽 끝(흙막이 없음)
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts"));
const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford_Geom.ts"));
const { computeBoxLayout } = loadTs(B06("logic/B06_Section_Cross_Box_Geom.ts"));
const { computeFordLayout } = loadTs(B06("logic/B06_Section_Cross_Ford_Geom.ts"));
function endCuts(base, compute) {
const s = clone(base);
delete s.culvert;
+2 -2
View File
@@ -1,6 +1,6 @@
"""PLAN 139-4 · 139-5 — 묻힌 흙막이 유출 끝 = 3도 수평 절토선(보라 · 절토 면적) · 못 만나면 복합경사.
짝: `B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts`(끝 구간 규칙) · `_Culvert_Tiers.ts`(채널 입력) ·
짝: `B06_Section/logic/B06_Section_Cross_Culvert_Extra.ts`(끝 구간 규칙) · `_Culvert_Tiers.ts`(채널 입력) ·
`_Culvert_Geom.ts`(유출 절토선 면적에 넣음) · `_Culvert.ts`(그림 · 툴팁). 실제 TS 를 `helper_139_4_buried_outlet.cjs` 가 돌린다.
"""
@@ -68,7 +68,7 @@ def test_유출_절토선은_그리고_면적에_넣음() -> None:
culvert = _src("B06_Section/B06_Section_UI_Cross_Culvert.ts")
assert 'if (segment.kind === "cut" && !culvert.body) continue;' not in culvert
assert 'planLine(segment.points, cutLineTip("유출부", segment));' in culvert
geom = _src("B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts")
geom = _src("B06_Section/logic/B06_Section_Cross_Culvert_Geom.ts")
assert "extendTrimSlope(drawnFillPoints(extrasForArea, true), outletInfo.outward);" in geom
# 집수정 계류측 3도 선은 그림만(면적 밖) — 세월교 · 박스 계류측만 면적
assert (
+1 -1
View File
@@ -51,5 +51,5 @@ def test_풀린_카드_이_측점_높이_줄() -> None:
dock = _src("B06_Section/B06_Section_UI_Adjust_Dock.ts")
assert 'const OWN_HEIGHT_ROW = ".b06-structure-panel__struct--own-height";' in dock
assert "return OWN_HEIGHT_ROW;" in dock # 세월교 · 박스 날개벽 칸도 받음
wire = _src("B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts")
wire = _src("B06_Section/logic/B06_Section_Cross_Culvert_Wire.ts")
assert "h: shift.inlet.h ?? heights.inlet" in wire
+2 -2
View File
@@ -1,7 +1,7 @@
"""PLAN 190-1 — B06 집수정 고르기 판정면 = 부재 외곽 가상 사각형(세월교 측벽 ㄴ 꼴 · 79-6).
짝: `B06_Section/B06_Section_UI_Cross_Culvert.ts`(집수정 판정면) · `_Cross_Ford.ts`(측벽 판정면) ·
`_Cross_Culvert_Types.ts`(`outlineBox` 한 벌). [상세] 모달은 카드 그림 복제라 같은 판정면을 가짐.
`logic/B06_Section_Cross_Culvert_Types.ts`(`outlineBox` 한 벌). [상세] 모달은 카드 그림 복제라 같은 판정면을 가짐.
실제 TS 를 `helper_190_1_basin_pick.cjs` 가 Node 로 돌린다.
"""
@@ -76,7 +76,7 @@ def test_고름_없으면_판정면_없음(out) -> None:
def test_세월교_측벽과_같은_함수() -> None:
types = _src("B06_Section/B06_Section_UI_Cross_Culvert_Types.ts")
types = _src("B06_Section/logic/B06_Section_Cross_Culvert_Types.ts")
assert "export function outlineBox(points: OffsetPoint[]): OffsetPoint[] {" in types
ford = _src("B06_Section/B06_Section_UI_Cross_Ford.ts")
assert 'polygon(outlineBox(points), "b06-chart__culvert-revet-hit", "")' in ford
+1 -1
View File
@@ -107,7 +107,7 @@ def test_서버_한쪽_이동_반대측_고정_바깥으로() -> None:
def test_구조물은_노견_끝에_섬() -> None:
"""관 기슭막이 자리 = 설계 노견 끝(`road_edges` · 노견 확장 포함) — 사면과 한 벌로 감."""
src = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Geom.ts").read_text(
src = (ROOT / "B06_Section" / "logic/B06_Section_Cross_Culvert_Geom.ts").read_text(
encoding="utf-8"
)
assert "const edges = section.design?.road_edges;" in src
+1 -1
View File
@@ -21,7 +21,7 @@ def test_구간값_제어에_culvertopt_큐_쓰기_없음():
span = _read("B06_Section_UI_Page_Span_Control.ts")
assert "culvertOptions" not in span
assert "update: (section, role" not in span
wire = _read("B06_Section_UI_Cross_Culvert_Wire.ts")
wire = _read("logic/B06_Section_Cross_Culvert_Wire.ts")
assert not re.search(r"^\s*update: \(section: CrossSection, role", wire, re.M)
view = _read("B06_Section_UI_Cross_View_Structure.ts")
assert "structureSpan?.update(" not in view
@@ -97,19 +97,7 @@ GEOMETRY_PULLS = (
"B05_Profile/B05_Profile_UI_Corridor_Structures_Path.ts",
"B06_Section/B06_Section_Structure_Layouts.ts",
"B06_Section/B06_Section_UI_Cross_Body_Guard.ts",
"B06_Section/B06_Section_UI_Cross_Box.ts",
"B06_Section/B06_Section_UI_Cross_Box_Geom.ts",
"B06_Section/B06_Section_UI_Cross_Culvert.ts",
"B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts",
"B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts",
"B06_Section/B06_Section_UI_Cross_Culvert_Tiers.ts",
"B06_Section/B06_Section_UI_Cross_Culvert_Types.ts",
"B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts",
"B06_Section/B06_Section_UI_Cross_Design_Surface.ts",
"B06_Section/B06_Section_UI_Cross_Excavation.ts",
"B06_Section/B06_Section_UI_Cross_Ford.ts",
"B06_Section/B06_Section_UI_Cross_Ford_Geom.ts",
"B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts",
"B06_Section/B06_Section_UI_Cross_Revetment.ts",
"B06_Section/B06_Section_UI_Cross_Wall.ts",
"B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts",
@@ -162,6 +162,7 @@ class Node {
}
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const pave = load(path.join(ROOT, "B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts"));
const paveGeo = load(path.join(ROOT, "B06_Section/logic/B06_Section_Cross_Ford_Pavement.ts"));
// +offset = 좌측 · 좌 성토 1:1.5 · 우 절토 오름 — 유입 = 표고 높은 쪽(설계선 끝 · 286-2) = 우 → 유출 = 좌
// `rightEnd` = 우 끝 표고 — 좌 끝(97)과 같으면 단면유형으로 가름(left_cut = 유입 좌 → 유출 우)
const design = (mode, rightEnd = 99) => ({
@@ -181,7 +182,7 @@ const run = (mode, withChute = true) => {
return band ? band.attrs.points : null;
};
process.stdout.write(JSON.stringify({
outLeft: pave.outletFillLine(design("left_cut", 97)), outRight: pave.outletFillLine(design("left_cut")),
outLeft: paveGeo.outletFillLine(design("left_cut", 97)), outRight: paveGeo.outletFillLine(design("left_cut")),
band: run("left_cut"), none: run("left_cut", false),
}));
"""
+2 -2
View File
@@ -78,9 +78,9 @@ def test_지형에_막힘_잔재_없음() -> None:
for name in (
"B06_Section_UI_Cross_Culvert.ts",
"B06_Section_UI_Cross_Revetment.ts",
"B06_Section_UI_Cross_Culvert_Wire.ts",
"logic/B06_Section_Cross_Culvert_Wire.ts",
"B06_Section_UI_Cross_Culvert_Types.ts",
"B06_Section_UI_Cross_Culvert_Extra.ts",
"logic/B06_Section_Cross_Culvert_Extra.ts",
):
source = (ROOT / "B06_Section" / name).read_text(encoding="utf-8")
assert "shiftBlockedM" not in source and "지형에 막힘" not in source, name
@@ -22,7 +22,7 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
MODULE = PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Extra.ts"
MODULE = PROJECT_ROOT / "B06_Section" / "logic/B06_Section_Cross_Culvert_Extra.ts"
# 상하 내림 2.0m → 하한 2.4m. 좌우 0.5m 를 요청해도 2.4m 로 밀려 나가야 한다.
REQUESTED_X = 0.5
@@ -79,7 +79,7 @@ def _run(tmp_path: Path, requested_x: float = REQUESTED_X) -> dict:
check=False,
capture_output=True,
)
compiled = next(out.rglob("B06_Section_UI_Cross_Culvert_Extra.js"), None)
compiled = next(out.rglob("B06_Section_Cross_Culvert_Extra.js"), None)
assert compiled is not None, "화면 코드가 JS 로 안 나옴 — tsc 실패"
(out / "runner.cjs").write_text(
_RUNNER % {"x": requested_x, "d": REQUESTED_D}, encoding="utf-8"
+9 -6
View File
@@ -18,8 +18,8 @@ BUNDLE = PROJECT_ROOT / "config" / "design_calc_node" / "design_calc_node.js"
_RUNNER = """
import { writeFileSync } from "node:fs";
import { placePipeWall } from "./logic/B06_Section_UI_Cross_Culvert_Solve.js";
import { buildOutletExtras } from "./B06_Section_UI_Cross_Culvert_Extra.js";
import { placePipeWall } from "./B06_Section_UI_Cross_Culvert_Solve.js";
import { buildOutletExtras } from "./B06_Section_Cross_Culvert_Extra.js";
const pipe = (d, x) => placePipeWall({
autoOffset: 10, outward: 1, height: 2, baseElevation0: 98, edgeElevation: 100,
invertCap: Number.POSITIVE_INFINITY, adjust: { x, d, h: null, m: null }, defaultD: 4.8,
@@ -42,7 +42,10 @@ writeFileSync(process.argv[2], JSON.stringify({
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_원지반이_자리_다단을_막지_않고_노면_위만_막음(tmp_path: Path) -> None:
out = tmp_path / "js"
for name in ("logic/B06_Section_UI_Cross_Culvert_Solve", "B06_Section_UI_Cross_Culvert_Extra"):
for name in (
"logic/B06_Section_UI_Cross_Culvert_Solve",
"logic/B06_Section_Cross_Culvert_Extra",
):
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
@@ -67,7 +70,7 @@ def test_원지반이_자리_다단을_막지_않고_노면_위만_막음(tmp_pa
re.sub(r'(from\s+"\.{1,2}/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
encoding="utf-8",
)
base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Culvert_Extra.js"))
base = next(path.parent for path in out.rglob("B06_Section_Cross_Culvert_Extra.js"))
(base / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
result = tmp_path / "out.json"
subprocess.run( # noqa: S603
@@ -87,7 +90,7 @@ def test_원지반이_자리_다단을_막지_않고_노면_위만_막음(tmp_pa
def test_화면_서버_같은_판() -> None:
for name in (
"logic/B06_Section_UI_Cross_Culvert_Solve",
"B06_Section_UI_Cross_Culvert_Geom",
"logic/B06_Section_Cross_Culvert_Geom",
"B06_Section_UI_Cross_Revetment",
):
source = (B06 / f"{name}.ts").read_text(encoding="utf-8")
@@ -96,7 +99,7 @@ def test_화면_서버_같은_판() -> None:
assert "const placeable" not in (B06 / "B06_Section_UI_Cross_Revetment.ts").read_text(
encoding="utf-8"
), "사면안정 흙막이 · 옹벽 매몰 자리 금지가 남음"
assert "const placeable" not in (B06 / "B06_Section_UI_Cross_Culvert_Extra.ts").read_text(
assert "const placeable" not in (B06 / "logic/B06_Section_Cross_Culvert_Extra.ts").read_text(
encoding="utf-8"
), "추가 흙막이 매몰 자리 금지가 남음"
bundle = BUNDLE.read_text(encoding="utf-8")
+9 -6
View File
@@ -18,8 +18,8 @@ BUNDLE = PROJECT_ROOT / "config" / "design_calc_node" / "design_calc_node.js"
_RUNNER = """
import { writeFileSync } from "node:fs";
import { revetWallSpec } from "./logic/B06_Section_UI_Cross_Culvert_Const.js";
import { buildOutletExtras } from "./B06_Section_UI_Cross_Culvert_Extra.js";
import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const.js";
import { buildOutletExtras } from "./B06_Section_Cross_Culvert_Extra.js";
const spec = { revet_form: "찰쌓기", revet_height_m: 1.5 };
const pipe = (h) => revetWallSpec(spec, { h, m: null }, false, 0.8).pureHeight;
const own = (h) => revetWallSpec(spec, { h, m: null }, true, 0).pureHeight;
@@ -41,7 +41,10 @@ writeFileSync(process.argv[2], JSON.stringify({
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_높이는_입력_그대로_0이하만_멈춤(tmp_path: Path) -> None:
out = tmp_path / "js"
for name in ("logic/B06_Section_UI_Cross_Culvert_Const", "B06_Section_UI_Cross_Culvert_Extra"):
for name in (
"logic/B06_Section_UI_Cross_Culvert_Const",
"logic/B06_Section_Cross_Culvert_Extra",
):
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
@@ -66,7 +69,7 @@ def test_높이는_입력_그대로_0이하만_멈춤(tmp_path: Path) -> None:
re.sub(r'(from\s+"\.{1,2}/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
encoding="utf-8",
)
base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Culvert_Extra.js"))
base = next(path.parent for path in out.rglob("B06_Section_Cross_Culvert_Extra.js"))
(base / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
result = tmp_path / "out.json"
subprocess.run( # noqa: S603
@@ -85,8 +88,8 @@ def test_높이는_입력_그대로_0이하만_멈춤(tmp_path: Path) -> None:
def test_높이_토스트_없음_서버_같은_판() -> None:
for name in ("UI_Cross_Culvert_Wire", "UI_Page_Pipe_Options"):
source = (B06 / f"B06_Section_{name}.ts").read_text(encoding="utf-8")
for name in ("logic/B06_Section_Cross_Culvert_Wire", "B06_Section_UI_Page_Pipe_Options"):
source = (B06 / f"{name}.ts").read_text(encoding="utf-8")
assert "B06_Cross_Height_Floor" not in source, f"{name} 에 최소 높이 토스트가 남음"
assert "높이는 최소" not in source, f"{name} 에 최소 높이 토스트가 남음"
panel = (B06 / "B06_Section_UI_Cross_Structure_Panel.ts").read_text(encoding="utf-8")
+2 -2
View File
@@ -19,9 +19,9 @@ if str(ROOT) not in sys.path:
DATA_DIR = ROOT / "resources" / "data_masonry"
GEOMETRY = [
"B06_Section_UI_Cross_Culvert_Geom.ts",
"logic/B06_Section_Cross_Culvert_Geom.ts",
"B06_Section_UI_Cross_Wall.ts",
"B06_Section_UI_Cross_Culvert_Extra.ts",
"logic/B06_Section_Cross_Culvert_Extra.ts",
"logic/B06_Section_UI_Cross_Culvert_Solve.ts",
]
@@ -95,12 +95,16 @@ def test_벽_외곽_한_벌_높이_입력_그대로_노면_멈춤(tmp_path: Path
def test_네_벽이_공용_함수로_서고_형태_한도가_없다() -> None:
for name in ("Culvert_Geom", "Culvert_Extra", "Revetment"):
source = (B06 / f"B06_Section_UI_Cross_{name}.ts").read_text(encoding="utf-8")
for name in (
"logic/B06_Section_Cross_Culvert_Geom",
"logic/B06_Section_Cross_Culvert_Extra",
"B06_Section_UI_Cross_Revetment",
):
source = (B06 / f"{name}.ts").read_text(encoding="utf-8")
assert "buildRevetWallGeometry(" in source, f"{name} 가 공용 벽 함수를 안 씀"
for path in B06.rglob("*.ts"): # logic/ 까지(348-1)
source = path.read_text(encoding="utf-8")
assert "revetHeightLimit" not in source, f"{path.name} 에 형태 높이 한도가 남음"
assert "materialLimit" not in source, f"{path.name} 에 재질 높이 한도가 남음"
extra = (B06 / "B06_Section_UI_Cross_Culvert_Extra.ts").read_text(encoding="utf-8")
extra = (B06 / "logic/B06_Section_Cross_Culvert_Extra.ts").read_text(encoding="utf-8")
assert "fitted" not in extra, "추가 흙막이가 원지반에 맞춰 늘어남"
@@ -151,7 +151,7 @@
.map((e) => e.name)
.filter((n) => n.includes(part))
.pop();
const geom = await import(modUrl("/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts"));
const geom = await import(modUrl("/B06_Section/logic/B06_Section_Cross_Culvert_Geom.ts"));
const key = Object.keys(sessionStorage).find((k) => k.includes("section-detail"));
const detail = JSON.parse(sessionStorage.getItem(key));
const forms = geom.REVET_FORMS;