From d3315da9935ba51c6db142f52308c19cb0f1b508 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 23 Oct 2026 19:22:27 +0900 Subject: [PATCH] =?UTF-8?q?refactor(472-3=20B06=20=EB=B2=BD=20=C2=B7=20?= =?UTF-8?q?=EA=B8=B0=EC=8A=AD=EB=A7=89=EC=9D=B4=20=C2=B7=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EA=B8=B0=ED=95=98?= =?UTF-8?q?=20logic=20=EB=B6=84=EB=A6=AC=20=C2=B7=20=EA=B7=B8=EB=A6=BC=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=9D=80=20=EA=B7=B8=EB=A6=AC=EA=B8=B0?= =?UTF-8?q?=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - logic 새 파일 다섯 — Cross_Wall_Geom(벽 1매) · Cross_Wall_Hatch_Geom(무늬 띠 · 물빼기 · 도수로 띠 · 원지반선) · Cross_Excavation_Geom(벽 터파기) · Cross_Design_Surface_Geom(암선 · 포장층) · Cross_Revetment_Geom(독립 기슭막이 배치) - logic 으로 옮김 — Body_Guard → Cross_Body_Guard · Structure_Layouts · Route_Carry_Patch(내용 그대로 · import 경로만) - inferStationInterval → common_util_station(글자표 없는 자리 · common_util_svg 는 다시 내보냄) — Node 묶음 ui_template_locale 끌림 끊음 - 부르는 쪽 import 줄 · 등록부 ts 경로 · 시험 · 도우미 경로 - test_472_3 GEOMETRY_PULLS 다섯 줄 걷음 · Wall_Hatch · Excavation · Design_Surface 세 줄은 Culvert · Box · Ford 그림 파일(518-2)이 그리기 함수로 부르는 동안 남김 Co-Authored-By: Claude Opus 5.5 --- A00_Common/design/design_shared.ts | 2 +- .../design/design_structure_pipe_calc.ts | 2 +- A00_Common/design/logic/design_calc_node.ts | 2 +- .../design/logic/design_calc_node_carry.ts | 2 +- .../design/logic/design_calc_node_cross.ts | 2 +- .../design/logic/design_calc_node_stages.ts | 2 +- .../logic/design_calc_registry_facility.ts | 2 +- .../B05_Profile_UI_Corridor_Structures.ts | 6 +- ...05_Profile_UI_Corridor_Structures_Chute.ts | 2 +- ...B05_Profile_UI_Corridor_Structures_Path.ts | 2 +- .../B05_Profile_UI_RouteEdit_Cross_Card.ts | 2 +- B06_Section/B06_Section_Api_Types.ts | 2 +- B06_Section/B06_Section_Cross_Refresh.ts | 2 +- B06_Section/B06_Section_Section_Adjusts.ts | 2 +- B06_Section/B06_Section_Section_Store.ts | 4 +- B06_Section/B06_Section_UI_Cross_Box_Geom.ts | 2 +- B06_Section/B06_Section_UI_Cross_Culvert.ts | 10 +- .../B06_Section_UI_Cross_Culvert_Extra.ts | 2 +- .../B06_Section_UI_Cross_Culvert_Geom.ts | 6 +- .../B06_Section_UI_Cross_Culvert_Wire.ts | 2 +- .../B06_Section_UI_Cross_Design_Surface.ts | 60 +--- .../B06_Section_UI_Cross_Excavation.ts | 40 +-- B06_Section/B06_Section_UI_Cross_Ford_Geom.ts | 2 +- .../B06_Section_UI_Cross_Ford_Pavement.ts | 3 +- B06_Section/B06_Section_UI_Cross_Revetment.ts | 334 +----------------- .../B06_Section_UI_Cross_Underdrain.ts | 3 +- B06_Section/B06_Section_UI_Cross_View.ts | 8 +- B06_Section/B06_Section_UI_Cross_Wall.ts | 86 +---- .../B06_Section_UI_Cross_Wall_Hatch.ts | 210 +---------- .../B06_Section_Cross_Body_Guard.ts} | 18 +- .../B06_Section_Cross_Design_Surface_Geom.ts | 71 ++++ .../B06_Section_Cross_Excavation_Geom.ts | 57 +++ .../logic/B06_Section_Cross_Revetment_Geom.ts | 334 ++++++++++++++++++ .../logic/B06_Section_Cross_Wall_Geom.ts | 89 +++++ .../B06_Section_Cross_Wall_Hatch_Geom.ts | 223 ++++++++++++ .../B06_Section_Route_Carry_Patch.ts | 28 +- .../B06_Section_Structure_Layouts.ts | 26 +- .../B07_DesignDetail_Cross_Overlay.ts | 4 +- common_util/common_util_station.ts | 16 + common_util/common_util_svg.ts | 18 +- resources/tester/helper_107_ford_free.cjs | 2 +- resources/tester/helper_118_pipe_pick.cjs | 2 +- resources/tester/helper_135_3_chute_solid.cjs | 2 +- resources/tester/helper_137_design_pick.cjs | 2 +- .../tester/helper_139_4_buried_outlet.cjs | 2 +- resources/tester/helper_152_2_link_height.cjs | 2 +- resources/tester/helper_152_3_chute_cut.cjs | 4 +- resources/tester/helper_152_4_chute_pick.cjs | 2 +- .../tester/helper_152_5_pavement_pick.cjs | 6 +- resources/tester/helper_178_3_slope_tiers.cjs | 2 +- resources/tester/helper_186_1_outlet_fold.cjs | 2 +- resources/tester/helper_190_1_basin_pick.cjs | 2 +- .../helper_280_3_route_undo_facilities.cjs | 2 +- .../tester/helper_286_1_ford_pavement.cjs | 11 +- .../tester/helper_294_3_ford_extend_chute.cjs | 2 +- .../tester/helper_315_ford_chute_overflow.cjs | 2 +- resources/tester/helper_318_cross_card.cjs | 17 +- resources/tester/helper_322_0_golden.cjs | 4 +- resources/tester/helper_349_0o_redo_same.cjs | 2 +- resources/tester/helper_349_0q_undo_echo.cjs | 2 +- resources/tester/helper_356_4_undo_grade.cjs | 2 +- resources/tester/helper_384_4_adjusts.cjs | 2 +- .../tester/helper_387_4b_pipe_options.cjs | 2 +- .../tester/helper_428_pipe_move_entry.ts | 2 +- resources/tester/helper_445_save_patch.cjs | 2 +- .../tester/helper_472_1_browser_entry.ts | 2 +- resources/tester/helper_472_6_route_entry.ts | 2 +- resources/tester/helper_73_4_body_guard.cjs | 4 +- resources/tester/helper_79_guard_shape.cjs | 6 +- resources/tester/helper_80_ford_pipe.cjs | 2 +- resources/tester/helper_91_ground_free.cjs | 2 +- resources/tester/test_131_b08_material_fix.py | 4 +- .../tester/test_178_1_slope_wall_guard.py | 2 +- .../tester/test_346_3_route_save_screen.py | 2 +- resources/tester/test_445_save_patch.py | 9 +- resources/tester/test_472_3_calc_registry.py | 11 +- resources/tester/test_511_a_calc_mirror.py | 2 +- resources/tester/test_511_c_haul_node.py | 2 +- resources/tester/test_b06_pipe_length.py | 2 +- resources/tester/test_b06_wall_drain_pipe.py | 5 +- resources/tester/test_b06_wall_lean_table.py | 2 +- .../tester/test_b06_wall_shape_unified.py | 11 +- 82 files changed, 1000 insertions(+), 840 deletions(-) rename B06_Section/{B06_Section_UI_Cross_Body_Guard.ts => logic/B06_Section_Cross_Body_Guard.ts} (95%) create mode 100644 B06_Section/logic/B06_Section_Cross_Design_Surface_Geom.ts create mode 100644 B06_Section/logic/B06_Section_Cross_Excavation_Geom.ts create mode 100644 B06_Section/logic/B06_Section_Cross_Revetment_Geom.ts create mode 100644 B06_Section/logic/B06_Section_Cross_Wall_Geom.ts create mode 100644 B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom.ts rename B06_Section/{ => logic}/B06_Section_Route_Carry_Patch.ts (95%) rename B06_Section/{ => logic}/B06_Section_Structure_Layouts.ts (94%) diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index fdcdab850..26c29cd71 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -45,7 +45,7 @@ import { SECTION_STATION_INTERVAL_M } from "../../B05_Profile/logic/B05_Profile_ import { applyStructureAreaRows, structureAreaRows, -} from "../../B06_Section/B06_Section_Structure_Layouts"; +} from "../../B06_Section/logic/B06_Section_Structure_Layouts"; import { withDraftAdjusts } from "../../B06_Section/B06_Section_Section_Adjusts"; import { holdSectionDetail, diff --git a/A00_Common/design/design_structure_pipe_calc.ts b/A00_Common/design/design_structure_pipe_calc.ts index de52b3b7b..261c5e75a 100644 --- a/A00_Common/design/design_structure_pipe_calc.ts +++ b/A00_Common/design/design_structure_pipe_calc.ts @@ -10,7 +10,7 @@ * ========================================================================== */ import type { CrossSection } from "../../B06_Section/B06_Section_Api_Fetch"; -import { computeStoredLayouts } from "../../B06_Section/B06_Section_Structure_Layouts"; +import { computeStoredLayouts } from "../../B06_Section/logic/B06_Section_Structure_Layouts"; export { DEFAULT_BASIN_SHAPE } from "../../B06_Section/logic/B06_Section_UI_Cross_Culvert_Basin"; export { pipeLengthText } from "../../B06_Section/logic/B06_Section_UI_Cross_Culvert_Const"; // 「8 m (7.6)」(225-1) import { PIPE_MATCH_M } from "./design_selection"; diff --git a/A00_Common/design/logic/design_calc_node.ts b/A00_Common/design/logic/design_calc_node.ts index 8d8f6ddc4..65da0c520 100644 --- a/A00_Common/design/logic/design_calc_node.ts +++ b/A00_Common/design/logic/design_calc_node.ts @@ -51,7 +51,7 @@ import { applyStructureAreaRows, extraWallRows, structureAreaRows, -} from "../../../B06_Section/B06_Section_Structure_Layouts"; +} from "../../../B06_Section/logic/B06_Section_Structure_Layouts"; import { carryRunners, type CarryInputs } from "./design_calc_node_carry"; import { crossRunners, PAGE_IDS, useDefinition, type CrossInputs } from "./design_calc_node_cross"; import { followRunners, type FollowInputs } from "./design_calc_node_follow"; diff --git a/A00_Common/design/logic/design_calc_node_carry.ts b/A00_Common/design/logic/design_calc_node_carry.ts index 38fa33c08..ca80e13a9 100644 --- a/A00_Common/design/logic/design_calc_node_carry.ts +++ b/A00_Common/design/logic/design_calc_node_carry.ts @@ -18,7 +18,7 @@ import { import { routeCarryPatches, type RouteCarryInput, -} from "../../../B06_Section/B06_Section_Route_Carry_Patch"; +} from "../../../B06_Section/logic/B06_Section_Route_Carry_Patch"; type Pipe = FollowingPoint & { id?: string | null }; type Structure = FollowingStructure & { structure_id?: string | null }; diff --git a/A00_Common/design/logic/design_calc_node_cross.ts b/A00_Common/design/logic/design_calc_node_cross.ts index 34d129ad8..cd3d79481 100644 --- a/A00_Common/design/logic/design_calc_node_cross.ts +++ b/A00_Common/design/logic/design_calc_node_cross.ts @@ -28,7 +28,7 @@ import { type RefreshInputs, } from "../../../B06_Section/logic/B06_Section_Cross_Refresh_Calc"; import type { SectionDetailResponse } from "../../../B06_Section/B06_Section_Api_Fetch"; -import { structureAreaRows } from "../../../B06_Section/B06_Section_Structure_Layouts"; +import { structureAreaRows } from "../../../B06_Section/logic/B06_Section_Structure_Layouts"; import { gridAt } from "./design_calc_node_stages"; export interface CrossInputs { diff --git a/A00_Common/design/logic/design_calc_node_stages.ts b/A00_Common/design/logic/design_calc_node_stages.ts index bec708668..472bc7562 100644 --- a/A00_Common/design/logic/design_calc_node_stages.ts +++ b/A00_Common/design/logic/design_calc_node_stages.ts @@ -47,7 +47,7 @@ import type { SectionDetailResponse, } from "../../../B06_Section/B06_Section_Api_Fetch"; import { createCrossStage } from "../../../B06_Section/logic/B06_Section_Cross_Stage"; -import { pipeBook } from "../../../B06_Section/B06_Section_Route_Carry_Patch"; +import { pipeBook } from "../../../B06_Section/logic/B06_Section_Route_Carry_Patch"; import { plantedBase } from "./design_planted_base"; import { AFTER_STAGES, BROWSER_STAGES, createDesignPipeline } from "./design_pipeline"; import { contextCross, placedFollowInputs } from "./design_pipeline_stage_inputs"; diff --git a/A00_Common/design/logic/design_calc_registry_facility.ts b/A00_Common/design/logic/design_calc_registry_facility.ts index c327f28a2..b84e9f8fb 100644 --- a/A00_Common/design/logic/design_calc_registry_facility.ts +++ b/A00_Common/design/logic/design_calc_registry_facility.ts @@ -125,7 +125,7 @@ export const FACILITY_ROWS: readonly CalcEntry[] = [ outputs: ["관 길이 · 구조물 면적 · 선 다단 벽", "db:cross_sections.design"], site: "browser_now", stage: null, - ts: `${B06}B06_Section_Structure_Layouts.ts#structureAreaRows`, + ts: `${B06}logic/B06_Section_Structure_Layouts.ts#structureAreaRows`, runner: "node:structure.layouts", pair: null, mirror: ["test_b06_pipe_length", "test_299_ford_span_pipe"], diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts index fbdeb6050..940d5b943 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts @@ -50,10 +50,10 @@ import { storedAdjusts, type WallPath, } from "./B05_Profile_UI_Corridor_Structures_Path"; -import { computeRevetmentLayout } from "../B06_Section/B06_Section_UI_Cross_Revetment"; -import { withoutGuardedFill } from "../B06_Section/B06_Section_UI_Cross_Body_Guard"; +import { computeRevetmentLayout } from "../B06_Section/logic/B06_Section_Cross_Revetment_Geom"; +import { withoutGuardedFill } from "../B06_Section/logic/B06_Section_Cross_Body_Guard"; import { chuteBands, fordPavementChute } from "./B05_Profile_UI_Corridor_Structures_Chute"; -import { isChuteSegment } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch"; +import { isChuteSegment } from "../B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom"; import { builds, culvertBody, ownsBody } from "./logic/B05_Profile_UI_Corridor_Structures_Pick"; /** 스윕 프레임 하나 — 노선 위 한 지점의 중심 XY·좌향 단위벡터와 종단 표고 오프셋. */ diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts index cedbba41f..633dfb711 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts @@ -14,7 +14,7 @@ import { chuteBandOutline, chuteRun, groundLine, -} from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch"; +} from "../B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom"; import { outletFillLine } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement"; /** 성토 조각 하나 — B06 오버레이가 띠를 그리는 선(절토 조각은 뺀다). */ diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Path.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Path.ts index 1ae8dd7a6..db8d8f6ec 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Path.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Path.ts @@ -13,7 +13,7 @@ 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 { bodyAbsolute } from "../B06_Section/B06_Section_UI_Cross_Body_Guard"; +import { bodyAbsolute } from "../B06_Section/logic/B06_Section_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 { diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts index f99e4050e..616986b81 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts @@ -23,7 +23,7 @@ import { extraWalls, inletStructure, revetOffset, -} from "../B06_Section/B06_Section_Structure_Layouts"; +} from "../B06_Section/logic/B06_Section_Structure_Layouts"; import { DEFAULT_BOX_SIDE_ADJUST } from "../B06_Section/B06_Section_UI_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"; diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index dde05d7ba..0059550db 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -59,7 +59,7 @@ export type { /** 물넘이포장 제원 — 정의처는 렌더 모듈이다(사본 금지, 타입 전용 import라 순환 없음). */ import type { FordPavementSpec } from "./B06_Section_UI_Cross_Ford_Pavement"; -import type { RevetmentSpec } from "./B06_Section_UI_Cross_Revetment"; +import type { RevetmentSpec } from "./logic/B06_Section_Cross_Revetment_Geom"; import type { PavementEntry } from "@util/common_util_structure_pavement"; import type { UnderdrainSpec } from "@util/common_util_structure_underdrain"; export type { FordPavementSpec, RevetmentSpec }; diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index bc5b86ae0..610385875 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -45,7 +45,7 @@ import type { ProfileAlignment } from "../B05_Profile/logic/B05_Profile_UI_Profi import { readAlignment, toDesignProfile } from "../B05_Profile/logic/B05_Profile_UI_Profile_Data"; import { useStationKeys } from "./B06_Section_Station_Keys"; import { hasStaleDesigns, staleKey } from "./B06_Section_UI_Section_Common"; -import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts"; +import { applyStructureAreaRows, structureAreaRows } from "./logic/B06_Section_Structure_Layouts"; import { calcOnServerNow } from "../A00_Common/design/design_calc_sites"; import { areasRequest, diff --git a/B06_Section/B06_Section_Section_Adjusts.ts b/B06_Section/B06_Section_Section_Adjusts.ts index eed1cfbb7..0efb16142 100644 --- a/B06_Section/B06_Section_Section_Adjusts.ts +++ b/B06_Section/B06_Section_Section_Adjusts.ts @@ -24,7 +24,7 @@ import { layPipeOptions, type Cells, type OptionPipe, -} from "./B06_Section_Route_Carry_Patch"; +} from "./logic/B06_Section_Route_Carry_Patch"; // 얹기 몸통 = `B06_Section_Route_Carry_Patch` 한 벌(서버 노선 바뀐 [저장] 넘김도 · 472-6) — 여기는 세션 겉. export { layPipeOptions, type OptionPipe }; diff --git a/B06_Section/B06_Section_Section_Store.ts b/B06_Section/B06_Section_Section_Store.ts index 548bc6fd1..3f3188268 100644 --- a/B06_Section/B06_Section_Section_Store.ts +++ b/B06_Section/B06_Section_Section_Store.ts @@ -26,9 +26,9 @@ import { withDraftShapes, withDraftStations, } from "./B06_Section_Section_Drafts"; -import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts"; +import { applyStructureAreaRows, structureAreaRows } from "./logic/B06_Section_Structure_Layouts"; import { withDraftAdjusts, withPipeOptions } from "./B06_Section_Section_Adjusts"; -import { crossPatchesFromCache } from "./B06_Section_Route_Carry_Patch"; +import { crossPatchesFromCache } from "./logic/B06_Section_Route_Carry_Patch"; // [저장] patch 바탕 = 한 벌(서버 노선 바뀐 [저장] 넘김도 · 472-6 6-B) export { crossPatchesFromCache }; diff --git a/B06_Section/B06_Section_UI_Cross_Box_Geom.ts b/B06_Section/B06_Section_UI_Cross_Box_Geom.ts index ff0268907..e1ae51215 100644 --- a/B06_Section/B06_Section_UI_Cross_Box_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Box_Geom.ts @@ -16,7 +16,7 @@ * 부재 두께는 세월교 승계(측벽 0.2·상판 0.3·저판 0.3)이며 백엔드가 실어 보낸다. * ========================================================================== */ -import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch"; +import { groundLine } from "./logic/B06_Section_Cross_Wall_Hatch_Geom"; import type { BoxSet, CrossSection, SectionSample } from "./B06_Section_Api_Fetch"; import type { BasinLayout, diff --git a/B06_Section/B06_Section_UI_Cross_Culvert.ts b/B06_Section/B06_Section_UI_Cross_Culvert.ts index 5a82725cb..4e5fd3b3a 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert.ts @@ -24,12 +24,10 @@ import type { } from "./B06_Section_UI_Cross_Culvert_Geom"; import { appendPipeInfoText, outlineBox } from "./B06_Section_UI_Cross_Culvert_Types"; import { pipeLengthText } from "./logic/B06_Section_UI_Cross_Culvert_Const"; -import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation"; -import { - appendChuteBand, - appendWallHatch, - isChuteSegment, -} from "./B06_Section_UI_Cross_Wall_Hatch"; +import { appendWallTrench } from "./B06_Section_UI_Cross_Excavation"; +import { foundationChoice } from "./logic/B06_Section_Cross_Excavation_Geom"; +import { appendChuteBand, appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch"; +import { isChuteSegment } from "./logic/B06_Section_Cross_Wall_Hatch_Geom"; // 기하 계산 진입점과 공개 상수·타입은 여기서 재수출한다 — B05(최소 토피)와 횡단 뷰가 // 이 모듈 경로로 이미 참조하고 있어 분리 후에도 import 경로를 바꾸지 않는다. diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts index facb70214..0132d70dc 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts @@ -32,7 +32,7 @@ import type { } 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"; +import { buildRevetWallGeometry } from "./logic/B06_Section_Cross_Wall_Geom"; export interface OutletExtrasInput { /** 성토부선 시작점 = 관 유출 하단 꼭짓점(배관 벽의 성토선 시작 자리). */ diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index cab7ffa11..cc050ba0a 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -6,7 +6,7 @@ * (designTrim). 유입 = 상단측. 집수정은 `_Basin.ts`, 성토부·다단은 `_Extra.ts`. * ========================================================================== */ -import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch"; +import { groundLine } from "./logic/B06_Section_Cross_Wall_Hatch_Geom"; import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch"; import { revetWallSpec, @@ -22,7 +22,7 @@ import { } 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"; +import { buildRevetWallGeometry } from "./logic/B06_Section_Cross_Wall_Geom"; import { bodyGuardAxes, bodyGuardBase, @@ -30,7 +30,7 @@ import { bodyGuardHeight, fordPipeFit, mergeGuardTrim, -} from "./B06_Section_UI_Cross_Body_Guard"; +} from "./logic/B06_Section_Cross_Body_Guard"; import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types"; import type { BasinLayout, diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts index 7e1a8b209..2ce433f85 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts @@ -4,7 +4,7 @@ * 분리(700줄 제한). 카드가 다시 그려질 때마다 한 번 불린다. * ========================================================================== */ -import { bodyAbsolute } from "./B06_Section_UI_Cross_Body_Guard"; +import { bodyAbsolute } from "./logic/B06_Section_Cross_Body_Guard"; import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch"; import { computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert"; import type { diff --git a/B06_Section/B06_Section_UI_Cross_Design_Surface.ts b/B06_Section/B06_Section_UI_Cross_Design_Surface.ts index dcc107c99..25b33d3b7 100644 --- a/B06_Section/B06_Section_UI_Cross_Design_Surface.ts +++ b/B06_Section/B06_Section_UI_Cross_Design_Surface.ts @@ -1,5 +1,9 @@ import type { PavementEntry } from "@util/common_util_structure_pavement"; import type { CrossDesign } from "./B06_Section_Api_Fetch"; +import { + pavementLayers, + rockBoundarySegments, +} from "./logic/B06_Section_Cross_Design_Surface_Geom"; const SVG_NS = "http://www.w3.org/2000/svg"; @@ -10,28 +14,17 @@ export function appendRockBoundaryOverlay( x: (offset: number) => number, y: (elevation: number) => number, ): void { - const segments: string[][] = []; - let current: string[] = []; - for (const sample of samples) { - const elevation = sample.elevation_m; - if (sample.valid === false || elevation === null || !Number.isFinite(elevation ?? NaN)) { - if (current.length > 1) segments.push(current); - current = []; - continue; - } - current.push(`${x(sample.offset_m ?? 0)},${y((elevation as number) + offsetM)}`); - } - if (current.length > 1) segments.push(current); - for (const points of segments) { + for (const segment of rockBoundarySegments(samples, offsetM)) { const polyline = document.createElementNS(SVG_NS, "polyline"); - polyline.setAttribute("points", points.join(" ")); + polyline.setAttribute( + "points", + segment.map((p) => `${x(p.offset_m)},${y(p.elevation_m)}`).join(" "), + ); polyline.setAttribute("class", "b06-chart__rock-boundary"); svg.append(polyline); } } -type Edge = { offset_m: number; elevation_m: number }; - /** 포장사양 → 무늬(88) — 콘크리트 빗금 · 아스콘 점 · 쇄석 돌. 그 밖 = 채움만. */ const PAVE_MARKS: Record = { 콘크리트: { size: 6, marks: '' }, @@ -49,21 +42,6 @@ const PAVE_MARKS: Record = { /** 무늬 id 는 문서 안에서 유일 — 카드 · 다시 그리기마다 새 번호(물넘이 빗금과 같은 방식). */ let paveSeq = 0; -/** 확폭 끈 구간 — 규격 폭 끝(좌 = left − 확폭좌 · 우 = right + 확폭우 · 표고는 두 끝 사이 직선). */ -function standardEdges(edges: { left: Edge; right: Edge }, design: CrossDesign) { - const { left, right } = edges; - const span = right.offset_m - left.offset_m || 1; - const at = (offset: number): Edge => ({ - offset_m: offset, - elevation_m: - left.elevation_m + ((right.elevation_m - left.elevation_m) * (offset - left.offset_m)) / span, - }); - return { - left: at(left.offset_m - (design.widening_left_m ?? 0)), - right: at(right.offset_m + (design.widening_right_m ?? 0)), - }; -} - type PaveMark = { type_id: string; start_m?: number | null; end_m?: number | null }; /** 포장층 범위 표식(종류 · 시작 · 끝)이 그 노면공인가 — 겹친 노면공 가름. */ @@ -97,8 +75,8 @@ export function pavementAt( } /** - * 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`section.pavement`)면 그 두께 · 무늬 · - * 확폭 켬/끔(여러 건은 아래로 차례로 쌓음) · 없으면 카드 포장 켬일 때 표준 두께 채움만. + * 포장층 — 층 꼭짓점 = `pavementLayers`(두께 · 확폭 켬/끔 · 차례로 쌓음) · 여기는 무늬 · 범위 표식 · + * 그리기. 없으면 카드 포장 켬일 때 표준 두께 채움만. */ export function appendPavementOverlay( svg: SVGElement, @@ -107,20 +85,8 @@ export function appendPavementOverlay( y: (elevation: number) => number, pavement?: readonly PavementEntry[] | null, ): void { - const edges = design.carriageway_edges ?? design.road_edges; - if (!edges) return; - const layers = pavement?.length ? pavement : design.paved ? [null] : []; - let depth = 0; - for (const entry of layers) { - const thickness = entry?.thickness_m ?? design.pavement_thickness_m ?? 0.2; - const { left, right } = entry && !entry.widening_on ? standardEdges(edges, design) : edges; - const points = [ - `${x(left.offset_m)},${y(left.elevation_m - depth)}`, - `${x(right.offset_m)},${y(right.elevation_m - depth)}`, - `${x(right.offset_m)},${y(right.elevation_m - depth - thickness)}`, - `${x(left.offset_m)},${y(left.elevation_m - depth - thickness)}`, - ].join(" "); - depth += thickness; + for (const { entry, corners } of pavementLayers(design, pavement)) { + const points = corners.map((p) => `${x(p.offset_m)},${y(p.elevation_m)}`).join(" "); const polygon = document.createElementNS(SVG_NS, "polygon"); polygon.setAttribute("points", points); polygon.setAttribute("class", "b06-chart__pavement"); diff --git a/B06_Section/B06_Section_UI_Cross_Excavation.ts b/B06_Section/B06_Section_UI_Cross_Excavation.ts index a432f412d..66754615f 100644 --- a/B06_Section/B06_Section_UI_Cross_Excavation.ts +++ b/B06_Section/B06_Section_UI_Cross_Excavation.ts @@ -4,6 +4,7 @@ * * 왜 있나 — 「터파기가 현재 횡단도에서 표현이 안 된다」가 사용자 지적이었다. 수량을 * 세기 전에 **도면에 그것이 보여야** 한다. + * 자리 · 폭 · 깊이 = `logic/B06_Section_Cross_Excavation_Geom`(472-3). * * ⚠ 근거의 급이 다르므로 **노티스를 갈라 적는다** — 값은 `common_util_excavation` 한 곳에서 * 온다(파이썬 짝과 거울 시험으로 묶여 있다). 여기서는 **그리기만** 한다. @@ -12,26 +13,12 @@ * ⚠ **비탈을 주지 않는다 — 수직으로 그린다.** 정본 식에 기울기 항이 없다. * ========================================================================== */ -import { - BASIS_WALL, - WALL_BLINDING_DEPTH_M, - WALL_BLINDING_WIDTH_M, - WALL_FOUNDATION_DEPTH_M, - WALL_FOUNDATION_WIDTH_M, - WALL_TRENCH_CLEARANCE_M, -} from "@util/common_util_excavation"; +import { BASIS_WALL } from "@util/common_util_excavation"; import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types"; +import { wallTrench } from "./logic/B06_Section_Cross_Excavation_Geom"; const SVG_NS = "http://www.w3.org/2000/svg"; -/** 「기초유」인가 — 저장 제원 `foundation` 의 값. 비어 있으면 **그리지 않는다**(근거 없음). */ -export function foundationChoice(value: unknown): "기초유" | "기초버림" | null { - const text = typeof value === "string" ? value.trim() : ""; - if (text === "기초유") return "기초유"; - if (text === "기초버림") return "기초버림"; - return null; -} - function trenchPath(layer: SVGElement, corners: Array<[number, number]>, tooltip: string): void { const line = document.createElementNS(SVG_NS, "polyline"); line.setAttribute("points", corners.map(([px, py]) => `${px},${py}`).join(" ")); @@ -43,7 +30,7 @@ function trenchPath(layer: SVGElement, corners: Array<[number, number]>, tooltip } /** - * 벽 터파기 — 벽 바닥선 아래로 **수직**. 폭은 바닥 폭 + 여유 0.2, 깊이는 기초 몫. + * 벽 터파기 — 벽 바닥선 아래로 **수직**(자리 = `wallTrench`). * `foundation` 이 비어 있으면 **그리지 않는다** — 사용자가 고르기 전에는 근거가 없다. */ export function appendWallTrench( @@ -53,18 +40,9 @@ export function appendWallTrench( x: (offset: number) => number, toDisplayY: (elevation: number) => number, ): { widthM: number; depthM: number } | null { - if (foundation === null) return null; - const hasFoundation = foundation === "기초유"; - const depth = hasFoundation ? WALL_FOUNDATION_DEPTH_M : WALL_BLINDING_DEPTH_M; - // 폭: 기초 몫은 정본이 정한 값(0.9 / 0.7), 다만 벽 바닥이 그보다 넓으면 그 폭 + 여유. - const baseWidth = Math.abs(wall.bottomFront.offset - wall.bottomBack.offset); - const minWidth = hasFoundation ? WALL_FOUNDATION_WIDTH_M : WALL_BLINDING_WIDTH_M; - const width = Math.max(minWidth, baseWidth + (hasFoundation ? WALL_TRENCH_CLEARANCE_M : 0)); - const center = (wall.bottomFront.offset + wall.bottomBack.offset) / 2; - const top = Math.max(wall.bottomFront.elevation, wall.bottomBack.elevation); - const bottom = top - depth; - const left = center - width / 2; - const right = center + width / 2; + const trench = wallTrench(wall, foundation); + if (!trench) return null; + const { left, right, top, bottom, widthM, depthM } = trench; trenchPath( layer, [ @@ -73,8 +51,8 @@ export function appendWallTrench( [x(right), toDisplayY(bottom)], [x(right), toDisplayY(top)], ], - `${foundation} 터파기 폭 ${width.toFixed(2)}m · 깊이 ${depth.toFixed(2)}m (수직)\n` + + `${foundation} 터파기 폭 ${widthM.toFixed(2)}m · 깊이 ${depthM.toFixed(2)}m (수직)\n` + `품셈에 규정이 없어 실무 관행으로 정한 값 — ${BASIS_WALL}`, ); - return { widthM: width, depthM: depth }; + return { widthM, depthM }; } diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Geom.ts b/B06_Section/B06_Section_UI_Cross_Ford_Geom.ts index dd456b134..821c74005 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Geom.ts @@ -19,7 +19,7 @@ * 성토측이라도 기슭막이 세트(다단·재질)와는 연계하지 않는다. * ========================================================================== */ -import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch"; +import { groundLine } from "./logic/B06_Section_Cross_Wall_Hatch_Geom"; 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 type { diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts index bd9d00b8d..e8f13ff6e 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts @@ -15,7 +15,8 @@ import type { CrossDesign } from "./B06_Section_Api_Fetch"; import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types"; 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"; +import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch"; +import { chuteRun } from "./logic/B06_Section_Cross_Wall_Hatch_Geom"; /** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */ export interface FordPavementSpec { diff --git a/B06_Section/B06_Section_UI_Cross_Revetment.ts b/B06_Section/B06_Section_UI_Cross_Revetment.ts index 8ac2c4316..f23868c7a 100644 --- a/B06_Section/B06_Section_UI_Cross_Revetment.ts +++ b/B06_Section/B06_Section_UI_Cross_Revetment.ts @@ -1,343 +1,21 @@ /* ============================================================================= * B06_Section_UI_Cross_Revetment.ts - * 독립 기슭막이(배관과 무관) — 배관 세트 기슭막이와 **같은 배치·같은 산식·같은 도형**. - * 배관만 없을 뿐, 벽 자리·이동·한계는 배관 유출 벽과 한 몸으로 푼다(2026-08-28 사용자: - * "배관용에서 배관만 빼면 대부분 동일해야"). - * - * · 자리(2026-08-29 사용자 개정): 조작은 **순수 좌우(x)·순수 상하(d)** 0.1m다. - * x = 자동 자리에서의 수평 이동, d = 노견에서 잰 수직 하강. 성토선 각도는 유지하고 - * **노견 확장 구간**이 길이를 흡수한다. 한계는 노견 안쪽 금지·확장 0(각도 유지)·매몰 금지. - * 배관 기준벽(`placePipeWall`)의 이동은 손대지 않는다. - * · 실제 적용값(한계 절삭 후)은 `appliedAdjust`로 돌려주고, 화면이 조작 저장소에 되받는다 - * — 눌러도 안 움직이는데 숫자만 커지는 것 방지(배관과 같은 syncApplied 규칙). - * · 다단: 배관과 **같은 함수** `buildExtrasAt`. 그리기(폴리곤·이음선·돌·성토부선)는 공용 - * `_Cross_Wall`. 노견→벽 성토선은 designTrim의 slope로 그린다. - * - * 3D 코리도는 `tiers[].polygon`·`span`을 그대로 스윕한다. + * 독립 기슭막이 **그리기** — 자리 · 단 · 트림(기하)은 `logic/B06_Section_Cross_Revetment_Geom`(472-3). + * 그리기(폴리곤·이음선·돌·성토부선)는 배관과 같은 공용 `_Cross_Wall`. * ========================================================================== */ import { tagShape } from "../common_util/common_util_pick_tag"; -import type { CrossSection } from "./B06_Section_Api_Fetch"; import { FILL_SLOPE_RATIO_MIN, - PIPE_WALL_DEFAULT_RUN_M, REVET_EMBED_DEPTH_M, - REVET_FILL_TOP_M, - WALL_MIN_PURE_HEIGHT_M, } from "./logic/B06_Section_UI_Cross_Culvert_Const"; -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 { - ZERO_ADJUST, - type CulvertDesignTrim, - type OutletFillSegment, - type WallAdjust, - type WallLayout, -} from "./B06_Section_UI_Cross_Culvert_Types"; -import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation"; -import { appendPlanLine, buildRevetWallGeometry, drawRevetWall } from "./B06_Section_UI_Cross_Wall"; +import type { RevetmentLayout } from "./logic/B06_Section_Cross_Revetment_Geom"; +import { foundationChoice } from "./logic/B06_Section_Cross_Excavation_Geom"; +import { appendWallTrench } from "./B06_Section_UI_Cross_Excavation"; +import { appendPlanLine, drawRevetWall } from "./B06_Section_UI_Cross_Wall"; const SVG_NS = "http://www.w3.org/2000/svg"; -/** 서버가 얹어 주는 독립 기슭막이 제원(`section.revetment`, 구조물 정본 D군). */ -export interface RevetmentSpec { - structure_id: string | null; - type_id: string; - name: string; - start_m: number; - end_m: number; - anchor_m: number; - form?: string | null; - height_m?: number | null; - /** 사용자가 고른 설치 측 — "좌" | "우". */ - side?: string | null; - /** 기초 축 — "기초유" | "기초버림". **비어 있으면 터파기를 안 그린다**(근거 없음). */ - foundation?: string | null; - /** 단 수(다단). 1이면 단일 벽. */ - tiers?: number | null; - /** 추가 단(2단부터) 형태 · 높이 — 좌측 폼 「추가 단」 상자(178-3). 빈 값 = 본 벽 형태 · 기본 높이. */ - extras?: ReadonlyArray<{ form: string | null; height_m: number | null }> | null; - /** (구 모델) 기준 올림·좌우 이동 — 배관식 전환 뒤 자리는 조정창 4축(x·d)이 정한다. */ - lift_m?: number | null; - shift_m?: number | null; - /** 전면 기울기를 B08 과 같은 칸으로 가르는 값 — 성토/절토 · 전면 기울기(2026-09-14 B5). */ - face_role?: string | null; - face_slope_ratio?: number | string | null; -} - -/** 조정창 조작값 중 이 벽이 쓰는 축 — 배관 벽과 같은 형태(x 좌우·d 사면·h 높이). */ -export interface RevetmentAdjust { - x?: number; - d?: number | null; - h?: number | null; -} - -export interface RevetPoint { - offset: number; - elevation: number; -} - -/** 한 단(段) — 3D 스윕용 폴리곤(배관 벽과 같은 합성 단면). */ -export interface RevetmentTier { - polygon: RevetPoint[]; -} - -export interface RevetmentLayout { - side: "left" | "right"; - /** 1단 벽 높이(m) — **바닥~상단**. 조정창 높이 표시·조작의 기준값. */ - heightM: number; - /** 이 단면의 누가거리(m) — 부족 안내를 측점 단위로 센다(재렌더 중복 방지). */ - chainageM: number; - /** 벽 구조물 id — 그린 벽 신원표(323-5). */ - structureId: string | null; - /** 종류 이름(옹벽 · 흙막이 · 돌쌓기 …) — 툴팁 글(323-7 · 「독립 기슭막이」로 뜨던 것). */ - name: string; - /** 사용자가 요청한 단 수 — 실제로 선 단 수(`walls.length`)와 다르면 자리가 부족한 것이다. */ - requestedTiers: number; - /** 1단 벽 이음선 상단점(성토 설계선 트림 경계). */ - top: RevetPoint; - /** 벽 목록(1단 + 다단) — 배관 벽과 같은 `WallLayout`, 그리기가 그대로 쓴다. */ - walls: WallLayout[]; - /** 기초 축 — 저장 칸과 **같은 글자**(`foundation`). 터파기 그림이 이 값으로 갈린다. */ - foundation: string | null; - /** 벽 사이·벽 아래 성토부선(배관 다단과 같은 체계). */ - fillSegments: OutletFillSegment[]; - /** 성토 설계선을 벽 상단에서 끊고 노견→벽 성토선을 대신 그리는 트림. */ - designTrim: CulvertDesignTrim; - /** 한계 절삭 후 **실제 적용된** 1단 조작값 — 화면이 조작 저장소에 되받는다. */ - appliedAdjust: WallAdjust; - /** 3D 스윕용 단 목록 — 벽 폴리곤 그대로. */ - tiers: RevetmentTier[]; - /** 기준 측점 전/후 점유 길이(m) — 3D 스윕 범위. */ - span: { beforeM: number; afterM: number }; - /** 기준 측점이 아닌 측점(구간 연동) — 주 형상과 색을 갈라 그림(79-6). */ - linked: boolean; -} - -/** 형태(구조물 옵션) → 재질(높이 한계·라벨). 메 계열은 dry, 찰=wet, 콘크리트=concrete. */ -function materialOfForm(form?: string | null): RevetMaterial { - if (!form) return "dry"; - if (form.includes("찰")) return "wet"; - if (form.includes("콘크리트")) return "concrete"; - return "dry"; // 메쌓기·돌망태·통나무·바자 — 메 계열(2.0m 한계) -} - -/** 유효 지반 샘플의 바깥 한계 offset(설치 측). 없으면 null. */ -function sampleLimit(section: CrossSection, outward: number): number | null { - const offsets = section.samples - .filter((s) => s.valid !== false && typeof s.offset_m === "number") - .map((s) => s.offset_m as number); - if (!offsets.length) return null; - return outward > 0 ? Math.max(...offsets) : Math.min(...offsets); -} - -/** - * 노면 횡단물매를 **바깥 방향 1m당 표고 변화**로 돌려준다(하향이면 음수). - * 노면은 측구 쪽으로 내려가는 단일 평면이라(엔진 `road_z`), 노견을 밖으로 연장하면 - * 그 평면을 그대로 타고 가야 각도가 맞는다(2026-08-29 사용자). - * 1순위는 **실제 그려진 노견 구간**(차도 끝 → 노면 끝)의 기울기 — 부호를 추측하지 - * 않는다. 노견 폭이 0이면 `cross_slope_pct`를 측구 방향 부호로 쓴다. - */ -function roadSlopePerOutward( - design: NonNullable, - side: "left" | "right", - outward: number, -): number { - const edge = design.road_edges?.[side]; - const inner = design.carriageway_edges?.[side]; - if (edge && inner) { - const run = (edge.offset_m - inner.offset_m) * outward; - if (run > 1e-6) return (edge.elevation_m - inner.elevation_m) / run; - } - const pct = Number(design.cross_slope_pct); - if (!Number.isFinite(pct)) return 0; - return (pct / 100) * (design.ditch_side === side ? -1 : 1); -} - -/** - * 독립 기슭막이 단면 — 제원·높이·설계선이 없거나 자리를 못 찾으면 null. - * 1단 벽 = 배관 유출 벽과 같은 `placePipeWall`+`buildRevetWallGeometry`, - * 다단 = 배관과 같은 `buildExtrasAt`. - */ -export function computeRevetmentLayout( - section: CrossSection, - adjust?: RevetmentAdjust | null, -): RevetmentLayout | null { - const spec = section.revetment; - const design = section.design; - if (!spec || !design) return null; - const rawHeight = adjust?.h ?? spec.height_m; - // 빈 칸만 안 그림 — 0 이하는 아래 하한(0.1)에서 멈춤(54-51). - if (rawHeight == null || !Number.isFinite(Number(rawHeight))) return null; - const requestedHeight = Number(rawHeight); - - // 설치 측이 비어 있으면 **성토가 나는 쪽**에 세운다 — 좌측 「구조물 배치」의 C군 벽 - // (옹벽·돌쌓기 등)에는 설치 측 칸이 없기 때문이다(2026-09-06). 양측 절토면 벽이 설 - // 자리가 없으므로 그리지 않는다(면적도 그대로). - const fillSide = (): "left" | "right" | null => { - if (design.section_mode === "left_cut") return "right"; - if (design.section_mode === "right_cut") return "left"; - if (design.section_mode === "both_fill") return "left"; - return null; - }; - const side = spec.side === "우" ? "right" : spec.side === "좌" ? "left" : fillSide(); - if (!side) return null; - const outward = side === "left" ? 1 : -1; - const edge = design.road_edges?.[side]; - const groundAt = groundInterpolator(section.samples); - const limit = sampleLimit(section, outward); - if (!edge || !groundAt || limit === null) return null; - - // 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준). - // 형태별 높이 한계 없음 — 사용자 입력 그대로(하한만 둠). - const pureHeight = Math.max(requestedHeight, WALL_MIN_PURE_HEIGHT_M); - /** 기준선(근입 위)~상단 — 도형 계산은 종전대로 이 값으로 한다. */ - const height = pureHeight - REVET_EMBED_DEPTH_M; - - // 전면 기울기 — 관 흙막이와 같은 `outlineLeanOf`(형태 무관 · 형태는 안 무늬만). 단면유형 · - // 설치 측 · 높이 · 사용자 칸은 그대로 탐. 수량(B08)은 자기 칸으로 따로 셈. - const lean = outlineLeanOf({ - height_m: requestedHeight, - section_mode: design.section_mode, - side: spec.side, - face_role: spec.face_role, - face_slope_ratio: spec.face_slope_ratio, - }); - const roadSlope = roadSlopePerOutward(design, side, outward); - const autoOffset = - edge.offset_m + outward * (fillWallBaseWidth(height, lean) / 2 - REVET_FILL_TOP_M); - // x 원점 = 성토사면(1:1.2) 경사길이 4m 지점 — 저장된 x 의 절대 자리 그대로(79b). - const autoRun = PIPE_WALL_DEFAULT_RUN_M; - /** - * 조작 축(2026-08-29 사용자): x = **순수 좌우(m)**, d = **노견에서 잰 순수 수직 - * 하강(m)**. 성토선 각도는 그대로 두고 **노견 확장 구간**이 길이를 흡수한다 - * (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다). - */ - // 손대지 않은 벽(x · d 빈 칸) = 성토사면 제일 위(노견 · 79-5) · 손으로 넣은 벽은 종전 기본(79b). - const untouched = adjust?.d == null && !adjust?.x; - const defaultDrop = untouched ? 0 : autoRun / FILL_SLOPE_RATIO_MIN; - /** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리 — 음수 = 원점보다 - * 사면 위(노견 쪽)라 제일 위 자리는 x = −autoRun 이다. */ - const xFloor = (drop: number): number => FILL_SLOPE_RATIO_MIN * drop - autoRun; - const requestedX = untouched ? Number.NEGATIVE_INFINITY : (adjust?.x ?? 0); - // 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). 노면 위 금지 = d ≥ 0. - const appliedDrop = Math.round(Math.max(adjust?.d ?? defaultDrop, 0) * 10) / 10; - // 0.1 눈금 반올림(종전과 같음) · 반올림이 하한 아래로 내리면 하한(제일 위 자리가 노견 안쪽으로 안 감). - const floor = xFloor(appliedDrop); - const appliedX = Math.max(Math.round(Math.max(requestedX, floor) * 10) / 10, floor); - - const anchorOffset = autoOffset + outward * (autoRun + appliedX); - const baseElevation = edge.elevation_m - appliedDrop - height; - const floatGapM = Math.max(0, baseElevation - groundAt(anchorOffset)); - - const material = materialOfForm(spec.form); - const lengthM = Number.isFinite(spec.end_m - spec.start_m) ? spec.end_m - spec.start_m : null; - const tier1 = buildRevetWallGeometry({ - anchor: { offset: anchorOffset, elevation: baseElevation }, - outward, - height, - material, - role: "outlet", - form: spec.form, - lengthM, - floatGapM, - lean, - roadElevation: edge.elevation_m, - }); - // 넣은 좌우 이동이 **각도 하한**에 눌려 통째로 무시됐으면 그 하한을 남긴다(2026-09-07). - // 실측 — d 2.6m 자리에서 하한이 1.0m 를 넘어 「1.0m 를 넣어도 0.00m 이동」이 났음. - if (requestedX > 0.005 && floor > requestedX + 0.005) { - tier1.shiftFloorM = Math.round(floor * 100) / 100; - } - - // 다단 — 배관과 같은 함수. 1단 벽 전면 하단 꼭짓점을 성토부선 시작점으로. - const requestedTiers = Math.max(1, Math.round(Number(spec.tiers) || 1)); - const extras = - requestedTiers > 1 - ? buildExtrasAt(tier1.bottomFront, { - startBottomElevation: tier1.bottomBack.elevation, - outward, - groundAt, - limitOffset: limit, - // 단마다 상자 형태 · 높이(178-3) — 자리(x · d)는 다단 자동 규칙 그대로 - adjusts: Array.from({ length: requestedTiers - 1 }, (_, i) => ({ - ...ZERO_ADJUST, - m: spec.extras?.[i]?.form ?? null, - h: spec.extras?.[i]?.height_m ?? null, - })), - // 단들도 1단과 같은 형태로 선다 — 안 넘기면 형태가 비어 늘 메쌓기로 - // 그려졌다(2026-08-30 사용자: 형태별 모양이 누락되는 경우). - ownerForm: spec.form, - equalize: false, - leanFor: (form, pureHeightM) => - outlineLeanOf({ - form, - height_m: pureHeightM, - section_mode: design.section_mode, - side: spec.side, - }), - }) - : { walls: [], segments: [], appliedAdjusts: [], addable: false }; - - const walls: WallLayout[] = [tier1, ...extras.walls]; - // 물빼기 파이프 단면 = 표 형상칸.weep(322-1 · 옹벽) - if (catalogRow(spec.type_id)?.shapeOpts.weep) for (const wall of walls) wall.retaining = true; - const jt = tier1.fillTop; - // 노견 → (물매를 탄 노견 연장) → 벽 이음선 상단점. 연장 구간은 노면 물매 그대로 가고, - // 꺾임점에서 성토 물매 1:1.2로 벽 상단에 닿는다(2026-08-29 사용자 확정). - // D = 노견에서 벽 상단까지 바깥 방향 거리, R0 = 노견과 벽 상단의 낙차 - // (D − t) = 1.2 × (R0 + roadSlope × t) → t = (D − 1.2·R0) / (1 + 1.2·roadSlope) - const edgePoint = { offset: edge.offset_m, elevation: edge.elevation_m }; - const D = (jt.offset - edge.offset_m) * outward; - const R0 = edge.elevation_m - jt.elevation; - const denominator = 1 + FILL_SLOPE_RATIO_MIN * roadSlope; - const breakRun = - R0 > 1e-6 && D > 1e-6 && Math.abs(denominator) > 1e-9 - ? Math.min(D, Math.max(0, (D - FILL_SLOPE_RATIO_MIN * R0) / denominator)) - : 0; - const slope = { - points: - breakRun > 1e-6 - ? [ - edgePoint, - { - offset: edge.offset_m + outward * breakRun, - elevation: edge.elevation_m + roadSlope * breakRun, - }, - jt, - ] - : [edgePoint, jt], - }; - const designTrim: CulvertDesignTrim = - outward > 0 - ? { minOffset: -Infinity, maxOffset: jt.offset, maxElevation: jt.elevation, maxSlope: slope } - : { minOffset: jt.offset, maxOffset: Infinity, minElevation: jt.elevation, minSlope: slope }; - - return { - side, - heightM: pureHeight, - chainageM: Number(section.chainage_m) || 0, - structureId: spec.structure_id ?? null, - name: spec.name || "벽", - requestedTiers, - top: jt, - walls, - foundation: typeof spec.foundation === "string" ? spec.foundation.trim() || null : null, - fillSegments: extras.segments, - designTrim, - appliedAdjust: { x: appliedX, d: appliedDrop, h: adjust?.h ?? null, m: null }, - tiers: walls.map((wall) => ({ polygon: wall.points })), - span: { - beforeM: Math.max(spec.anchor_m - spec.start_m, 0), - afterM: Math.max(spec.end_m - spec.anchor_m, 0), - }, - linked: Math.abs((Number(section.chainage_m) || 0) - spec.anchor_m) > 0.02, - }; -} - /** 횡단 카드에 독립 기슭막이(1단+다단)를 그린다 — 배관 벽과 같은 도형. 그렸으면 setter. */ export function appendRevetmentOverlay( svg: SVGElement, diff --git a/B06_Section/B06_Section_UI_Cross_Underdrain.ts b/B06_Section/B06_Section_UI_Cross_Underdrain.ts index 4b43d4798..d1e0b6fe9 100644 --- a/B06_Section/B06_Section_UI_Cross_Underdrain.ts +++ b/B06_Section/B06_Section_UI_Cross_Underdrain.ts @@ -14,7 +14,8 @@ import type { CrossDesign } from "./B06_Section_Api_Fetch"; import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types"; -import { cell, courses, type BandFrame } from "./B06_Section_UI_Cross_Wall_Hatch"; +import { cell, type BandFrame } from "./B06_Section_UI_Cross_Wall_Hatch"; +import { courses } from "./logic/B06_Section_Cross_Wall_Hatch_Geom"; import type { UnderdrainSpec } from "@util/common_util_structure_underdrain"; const SVG_NS = "http://www.w3.org/2000/svg"; diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index 3f1ad8390..293964374 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -7,7 +7,7 @@ * 횡단은 종/횡 공통 Y스케일을 쓰지 않고 카드마다 자체 1:1 ppm으로 스케일한다. * ========================================================================== */ -import { withoutGuardedFill } from "./B06_Section_UI_Cross_Body_Guard"; +import { withoutGuardedFill } from "./logic/B06_Section_Cross_Body_Guard"; import { markLinkedFordPavement } from "./B06_Section_UI_Cross_Ford_Linked"; import type { CrossSection } from "./B06_Section_Api_Fetch"; import { @@ -24,8 +24,10 @@ import { attachShapePick, shapePickRoute, splitPipePick } from "./B06_Section_UI import { stackPickTargets } from "./B06_Section_UI_Cross_Pick_Order"; import { appendUnderdrainOverlay } from "./B06_Section_UI_Cross_Underdrain"; import { raiseWalls } from "./B06_Section_UI_Cross_Wall"; -import { attachChuteBandPick, groundLine } from "./B06_Section_UI_Cross_Wall_Hatch"; -import { appendRevetmentOverlay, computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment"; +import { attachChuteBandPick } from "./B06_Section_UI_Cross_Wall_Hatch"; +import { groundLine } from "./logic/B06_Section_Cross_Wall_Hatch_Geom"; +import { appendRevetmentOverlay } from "./B06_Section_UI_Cross_Revetment"; +import { computeRevetmentLayout } from "./logic/B06_Section_Cross_Revetment_Geom"; import { appendCrossDesignOverlay, appendPavementOverlay, diff --git a/B06_Section/B06_Section_UI_Cross_Wall.ts b/B06_Section/B06_Section_UI_Cross_Wall.ts index ae56ba825..6623be2d5 100644 --- a/B06_Section/B06_Section_UI_Cross_Wall.ts +++ b/B06_Section/B06_Section_UI_Cross_Wall.ts @@ -1,7 +1,7 @@ /* ============================================================================= * B06_Section_UI_Cross_Wall.ts - * 기슭막이 벽 **공용 기하 + 그리기** — 배관 세트 벽(`_Cross_Culvert_Geom`/`_Culvert`)과 - * 독립 기슭막이(`_Cross_Revetment`)가 같은 산식·같은 도형을 쓰도록 뽑아낸 층이다 + * 기슭막이 벽 **그리기**(기하 = `logic/B06_Section_Cross_Wall_Geom` · 472-3) — 배관 세트 벽 + * (`_Cross_Culvert_Geom`/`_Culvert`)과 독립 기슭막이(`_Cross_Revetment`)가 같은 산식·같은 도형을 쓰도록 뽑아낸 층이다 * (2026-08-28 사용자: "배관용에서 배관만 빼면 대부분 동일해야"). 벽 1매의 합성 단면 * (하부 사다리꼴 + 상부 평행사변형, 배면 수직·전면 1:0.3)과 그 돌쌓기 표현은 여기 하나뿐. * @@ -9,15 +9,6 @@ * 형태가 바꾸는 것은 안 무늬(`_Cross_Wall_Hatch`)뿐 — 외곽은 높이·자리·기울기로만 정함. * ========================================================================== */ -import { - materialLabel, - REVET_EMBED_DEPTH_M, - REVET_LEAN_RATIO, - REVET_THICKNESS_M, - WALL_FILL_TOP_INSET_M, - 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 { appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch"; @@ -39,79 +30,6 @@ function polygon( return shape; } -/** 벽 1매의 기하 입력 — 자리 기준은 **하단선 중점**(anchor.offset)과 그 자리 기준표고 - * (anchor.elevation = 관 invert 또는 지반). 벽은 그 위로 `height`만큼 선다. */ -export interface RevetWallGeometryInput { - /** 하단선 중점 offset + 기준표고(벽이 이 위로 선다). */ - anchor: OffsetPoint; - /** +1 = 화면 좌측(계류측), −1 = 우측. 전면 1:0.3이 이 부호로 기운다. */ - outward: number; - /** 계산용 높이(기준표고~상단, 근입 0.5 제외). */ - height: number; - material: RevetMaterial; - role: "inlet" | "outlet" | "extra"; - /** 표기용 형태 — 미지정이면 재질 라벨(메/찰/콘크리트). */ - form?: string | null; - lengthM?: number | null; - thickness?: number; - floatGapM?: number; - extraIndex?: number; - /** 전면 기울기 1:n 의 n — 표준경사(`leanOf`). 없으면 종전 0.3. */ - lean?: number; - /** 노면(계획고) — 벽 윗면이 이보다 높으면 여기서 멈춤. 높이 한도는 이것 하나. */ - roadElevation?: number | null; -} - -/** - * 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점을 만든다. 배관 벽(`buildWall`, - * `_Cross_Culvert_Geom` 335~376행)과 **같은 산식** — 배면 수직, 상단 변 = 사다리꼴 - * 상단(t/2)+띠(t), 전면 1:0.3, 하단선 = 기준표고 −0.5m 수평 기초. - */ -export function buildRevetWallGeometry(input: RevetWallGeometryInput): WallLayout { - const { anchor, outward, material, role } = input; - const road = input.roadElevation; - const height = - road != null && anchor.elevation + input.height > road + 1e-9 - ? Math.max(road - anchor.elevation, WALL_MIN_PURE_HEIGHT_M - REVET_EMBED_DEPTH_M) - : input.height; - const thickness = input.thickness ?? REVET_THICKNESS_M; - const lean = input.lean ?? REVET_LEAN_RATIO; - const baseWidth = thickness * 1.5 + lean * height; - const backOffset = anchor.offset - outward * (baseWidth / 2); - const topJoint = backOffset + outward * (thickness / 2); - const topElevation = anchor.elevation + height; - const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation }; - const topFront = topJoint + outward * thickness; - const frontXAt = (elevation: number): number => - topFront + outward * lean * (topElevation - elevation); - const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M; - const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation }; - const bottomFront: OffsetPoint = { - offset: frontXAt(bottomElevation), - elevation: bottomElevation, - }; - return { - role, - extraIndex: input.extraIndex, - form: input.form ?? materialLabel(material), - lengthM: input.lengthM ?? null, - backOffset, - outerOffset: bottomFront.offset, - base: anchor.elevation, - height, - floatGapM: input.floatGapM ?? 0, - material, - lean, - outward, - topBack, - topJoint: { offset: topJoint, elevation: topElevation }, - fillTop: { offset: topFront - outward * WALL_FILL_TOP_INSET_M, elevation: topElevation }, - bottomBack, - bottomFront, - points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront], - }; -} - /** * 벽 1매를 SVG로 그린다 — 폴리곤 + 대각 이음선 + 계류측 기운 띠의 돌쌓기 해칭. * 배관 오버레이(`_Cross_Culvert` 161~246행)와 **같은 그리기**. 선택·강조 배선은 호출자 diff --git a/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts b/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts index 38ac262c8..e8e857647 100644 --- a/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts +++ b/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts @@ -14,10 +14,20 @@ * · 돌망태 : 2열 격자 칸(철망 상자) — 칸마다 각진 사각형. * · 통나무·목재틀 : 원(통나무 마구리)을 한 줄로 쌓는다. * · 바자 : 세로 말뚝 3줄 + 가로 엮음 눈금. + * + * 띠 · 물빼기 · 도수로 띠 기하(m 좌표)는 `logic/B06_Section_Cross_Wall_Hatch_Geom`(472-3). * ========================================================================== */ import { REVET_THICKNESS_M } from "./logic/B06_Section_UI_Cross_Culvert_Const"; import type { OffsetPoint, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types"; +import { + chuteBandOutline, + chuteRun, + courses, + hatchOfForm, + wallDrainRuns, + wallHatchBand, +} from "./logic/B06_Section_Cross_Wall_Hatch_Geom"; const SVG_NS = "http://www.w3.org/2000/svg"; const HATCH_CLASS = "b06-chart__culvert-stone"; @@ -30,19 +40,6 @@ const HATCH_CLASS = "b06-chart__culvert-stone"; */ let clipSeq = 0; -/** 형태 문자열 → 표현 갈래. 판정 순서는 좁은 말(돌망태·바자)부터. */ -export type WallHatch = "dry" | "wet" | "concrete" | "gabion" | "log" | "wattle"; - -export function hatchOfForm(form: string | null | undefined): WallHatch { - if (!form) return "dry"; - if (form.includes("돌망태")) return "gabion"; - if (form.includes("통나무") || form.includes("목재")) return "log"; - if (form.includes("바자")) return "wattle"; - if (form.includes("콘크리트")) return "concrete"; - if (form.includes("찰")) return "wet"; - return "dry"; -} - /** 띠 안의 국소 좌표 → 화면 px. u = 바닥(0)~상단(1), v = 배면(-0.5)~전면(+0.5). */ export interface BandFrame { point: (u: number, v: number) => [number, number]; @@ -86,11 +83,6 @@ export function cell( return rect; } -/** 켜 수 — 켜 높이가 목표치에 가장 가깝게, 최소 2켜. */ -export function courses(spanM: number, targetM: number): number { - return Math.max(2, Math.round(spanM / targetM)); -} - /** 돌 한 줄 — `thicknessM` = 띠 두께(벽 = 흙막이 띠 · 도수로/돌붙임 = 그 두께 · 90-2). */ export function drawStones( group: SVGElement, @@ -158,70 +150,23 @@ function drawWattle(group: SVGElement, frame: BandFrame): void { } } -/** 물빼기 파이프 단면(73-2) — 보기용 고정값 · 지식DB 관경 · 간격과 별개. */ -const DRAIN_BAND_M = 1.5; -const DRAIN_DIAMETER_M = 0.15; -/** 앞쪽이 낮게 — 벽 두께 1m 당 낙차(보기용). */ -const DRAIN_FALL = 0.1; +/** 물빼기 파이프 단면(73-2) — 보기용 · 자리는 `wallDrainRuns`. */ const DRAIN_CLASS = "b06-chart__wall-drain"; -/** 직선 a→b 가 볼록 벽 단면 안을 지나는 구간 — clip 이 없는 곳(B07 오프스크린 수확)도 벽 밖으로 안 샘. */ -function insideRun(a: OffsetPoint, b: OffsetPoint, polygon: OffsetPoint[]): OffsetPoint[] | null { - const dx = b.offset - a.offset; - const dy = b.elevation - a.elevation; - const hits: number[] = []; - polygon.forEach((p, i) => { - const q = polygon[(i + 1) % polygon.length]; - const ex = q.offset - p.offset; - const ey = q.elevation - p.elevation; - const denominator = dx * ey - dy * ex; - if (Math.abs(denominator) < 1e-12) return; - const t = ((p.offset - a.offset) * ey - (p.elevation - a.elevation) * ex) / denominator; - const u = ((p.offset - a.offset) * dy - (p.elevation - a.elevation) * dx) / denominator; - if (u >= -1e-9 && u <= 1 + 1e-9) hits.push(t); - }); - if (hits.length < 2) return null; - const at = (t: number): OffsetPoint => ({ - offset: a.offset + dx * t, - elevation: a.elevation + dy * t, - }); - return [at(Math.min(...hits)), at(Math.max(...hits))]; -} - -/** - * 옹벽 물빼기 — 높이(바닥~상단)를 반올림(높이 / 1.5)개(최소 1) 띠로 고르게 나눠 띠마다 - * 가운데 하나. 배면 → 전면으로 꿰는 두 줄 평행선(앞쪽 조금 낮게) · 벽 두께만큼. - */ +/** 옹벽 물빼기 — 물빼기마다 두 줄 평행선(벽 안 구간 · `wallDrainRuns`). */ function drawDrains( group: SVGElement, wall: WallLayout, x: (offset: number) => number, y: (elevation: number) => number, ): void { - const bottom = wall.bottomBack.elevation; - const height = wall.topBack.elevation - bottom; - const count = Math.max(1, Math.round(height / DRAIN_BAND_M)); - const back = wall.backOffset; - const [, , topFront, bottomFront] = wall.points; - for (let i = 0; i < count; i += 1) { - const center = bottom + (height * (i + 0.5)) / count; - // 그 높이의 전면 — 전면은 기울어 위로 갈수록 배면에 가까움. - const front = - bottomFront.offset + (topFront.offset - bottomFront.offset) * ((center - bottom) / height); - const fall = (DRAIN_FALL * Math.abs(front - back)) / 2; + for (const runs of wallDrainRuns(wall)) { const pipe = document.createElementNS(SVG_NS, "g"); pipe.setAttribute("class", DRAIN_CLASS); const title = document.createElementNS(SVG_NS, "title"); title.textContent = "물빼기(보기용)"; pipe.append(title); - for (const side of [-0.5, 0.5]) { - const level = center + side * DRAIN_DIAMETER_M; - const run = insideRun( - { offset: back, elevation: level + fall }, - { offset: front, elevation: level - fall }, - wall.points, - ); - if (!run) continue; + for (const run of runs) { const el = line( [x(run[0].offset), y(run[0].elevation)], [x(run[1].offset), y(run[1].elevation)], @@ -244,15 +189,6 @@ export function appendWallHatch( toDisplayY: (elevation: number) => number, keyId: string, ): void { - // 하단선이 원지반을 따라 기울어 있으므로 띠 바닥도 하단선 위에서 잡는다. - const bottomSpan = wall.bottomFront.offset - wall.bottomBack.offset; - const bottomAtOffset = (offset: number): number => - Math.abs(bottomSpan) > 1e-9 - ? wall.bottomBack.elevation + - (wall.bottomFront.elevation - wall.bottomBack.elevation) * - ((offset - wall.bottomBack.offset) / bottomSpan) - : wall.bottomBack.elevation; - // 벽을 가로지르던 대각 이음선은 **긋지 않는다**(2026-09-12 사용자: 필요 없음). // 종전에는 상단 변 중간점에서 바닥까지 형태와 무관하게 늘 그어, 돌쌓기·콘크리트 해칭 // 위로 선이 하나 더 지나가 도면이 지저분했다. @@ -271,10 +207,7 @@ export function appendWallHatch( group.setAttribute("clip-path", `url(#${clipId})`); layer.append(clip, group); - const centerBase = wall.outerOffset - wall.outward * (REVET_THICKNESS_M / 2); - const centerTop = wall.topJoint.offset + wall.outward * (REVET_THICKNESS_M / 2); - const embedBase = bottomAtOffset(centerBase); - const spanM = Math.max(0.3, wall.topBack.elevation - embedBase); + const { centerBase, centerTop, embedBase, spanM } = wallHatchBand(wall); const pixelsPerMeter = Math.abs(x(1) - x(0)) || 1; const frame: BandFrame = { spanM, @@ -315,119 +248,6 @@ export function appendWallHatch( if (wall.retaining) drawDrains(group, wall, x, toDisplayY); } -/** - * 도수로 · 돌붙임 띠 단면(90-2) — 윗면 = 사면선(겹친 점 뺌) · 변마다 아래쪽(사면 안쪽) 단위 법선 · - * 둘레 = 윗면 → 두께만큼 오프셋한 아랫면(거꾸로). 횡단 그림과 B05 3D 띠 솔리드가 같은 것을 쓴다. - */ -export function chuteBandOutline( - raw: OffsetPoint[], - thickness: number, -): { top: OffsetPoint[]; normals: OffsetPoint[]; outline: OffsetPoint[] } | null { - if (!(thickness > 0)) return null; - const top = raw.filter( - (p, i) => - i === 0 || - Math.hypot(p.offset - raw[i - 1].offset, p.elevation - raw[i - 1].elevation) > 1e-6, - ); - if (top.length < 2) return null; - // 변마다 아래쪽(사면 안쪽) 단위 법선 - const normals = top.slice(1).map((p, i) => { - const dx = p.offset - top[i].offset; - const dz = p.elevation - top[i].elevation; - const length = Math.hypot(dx, dz); - const sign = dx / length > 0 ? -1 : 1; - return { offset: (sign * -dz) / length, elevation: (sign * dx) / length }; - }); - // 꺾임은 두 오프셋 선의 교점(마이터 · 너무 뾰족하면 두 배까지) - const bottom = top.map((p, i) => { - const a = normals[Math.max(i - 1, 0)]; - const b = normals[Math.min(i, normals.length - 1)]; - const sum = { offset: a.offset + b.offset, elevation: a.elevation + b.elevation }; - const k = thickness / Math.max(sum.offset * a.offset + sum.elevation * a.elevation, 0.5); - return { offset: p.offset + sum.offset * k, elevation: p.elevation + sum.elevation * k }; - }); - return { top, normals, outline: [...top, ...bottom.reverse()] }; -} - -/** 꺾은선 앞에서부터 `lengthM` 까지(쓴 길이와 함께). */ -function trimPolyline( - points: readonly OffsetPoint[], - lengthM: number, -): { points: OffsetPoint[]; used: number } { - const out = points.slice(0, 1); - let used = 0; - for (let i = 1; i < points.length && lengthM - used > 1e-9; i += 1) { - const [a, b] = [points[i - 1], points[i]]; - const step = Math.hypot(b.offset - a.offset, b.elevation - a.elevation); - if (step < 1e-9) continue; - const t = Math.min((lengthM - used) / step, 1); - out.push({ - offset: a.offset + (b.offset - a.offset) * t, - elevation: a.elevation + (b.elevation - a.elevation) * t, - }); - used += step * t; - } - return { points: out, used }; -} - -/** 원지반 샘플 → 꺾은선(유효 점만 · 오프셋 차례). */ -export function groundLine( - samples: ReadonlyArray<{ - offset_m?: number | null; - elevation_m?: number | null; - valid?: boolean; - }>, -): OffsetPoint[] { - return samples - .filter((s) => s.valid !== false && s.offset_m != null && s.elevation_m != null) - .map((s) => ({ offset: s.offset_m as number, elevation: s.elevation_m as number })) - .sort((a, b) => a.offset - b.offset); -} - -/** - * 도수로 · 돌붙임 선 길이(294-4) — 선들을 차례로 사면 따라 `lengthM` 까지 자름 · 사면보다 길면 마지막 끝에서 - * 원지반(`ground`)을 바깥으로 따라 이어 그 길이까지 · 길이 없으면 그대로(원지반 닿으면 끝). 점 하나뿐인 선 - * (사면 없음 · 끝이 이미 원지반)은 길이가 있으면 원지반 따라 · 없으면 안 그림. 그림 전용(수량 · 내역 무관). - */ -export function chuteRun( - lines: readonly L[], - lengthM?: number | null, - ground?: readonly OffsetPoint[], -): L[] { - const drawn = (list: L[]) => list.filter((line) => line.points.length > 1); - if (!(lengthM && lengthM > 0) || !lines.length) return drawn([...lines]); - let left = lengthM; - const out: L[] = []; - for (const line of lines) { - if (left <= 1e-9) break; - const cut = trimPolyline(line.points, left); - out.push({ ...line, points: cut.points }); - left -= cut.used; - } - const last = out[out.length - 1]; - if (left > 1e-9 && ground?.length && last) { - const first = lines[0].points[0]; - const end = last.points[last.points.length - 1]; - const outward = Math.sign(end.offset - first.offset) || Math.sign(end.offset) || 1; - const along = [ - end, - ...ground - .filter((p) => (p.offset - end.offset) * outward > 1e-6) - .sort((a, b) => (a.offset - b.offset) * outward), - ]; - out.push({ ...last, points: trimPolyline(along, left).points }); - } - return drawn(out); -} - -/** 띠가 서는 유출 조각(152-3) — 성토 조각 + 내림 · 수평 절토 조각(3도 절토선 · 끝 ≤ 시작). - * 3도 선이 못 만나 복합경사로 오르는 절토 조각은 뺌. B06 관 · 흙막이 띠와 3D 띠가 같은 거름. */ -export function isChuteSegment(segment: { kind?: string; points: OffsetPoint[] }): boolean { - if (segment.kind !== "cut") return true; - const { points } = segment; - return points.length > 1 && points[points.length - 1].elevation <= points[0].elevation + 1e-6; -} - /** * 도수로 · 돌붙임 띠(90-2) — 유출 성토사면 선(윗면 = 사면선)을 사면 안쪽(아래)으로 두께만큼 오프셋한 * 띠 · 안에 돌(흙막이 돌과 같은 그리기 · 두께 인자). 선마다 한 띠 · 그림만(면적 · 터파기 무관). diff --git a/B06_Section/B06_Section_UI_Cross_Body_Guard.ts b/B06_Section/logic/B06_Section_Cross_Body_Guard.ts similarity index 95% rename from B06_Section/B06_Section_UI_Cross_Body_Guard.ts rename to B06_Section/logic/B06_Section_Cross_Body_Guard.ts index 83e6b671b..72c989b05 100644 --- a/B06_Section/B06_Section_UI_Cross_Body_Guard.ts +++ b/B06_Section/logic/B06_Section_Cross_Body_Guard.ts @@ -1,5 +1,5 @@ /* ============================================================================= - * B06_Section_UI_Cross_Body_Guard.ts + * B06_Section_Cross_Body_Guard.ts * 세월교 · 박스 유입 · 유출 흙막이(73-4) — 관보호공 흙막이와 **같은 벽 · 같은 자리 규칙**. * * 세트는 배관 흙막이와 같은 `section.culvert`(관 숨김 · `body`)로 얹혀 그림 · 연동 · 경사 · 3D · @@ -10,20 +10,20 @@ * 본체가 이 측점에 없으면(연동 옆 측점) 배관 규칙 그대로 — 소유 측점 값을 절대값으로 넘겨 받음. * ========================================================================== */ -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 { FILL_SLOPE_RATIO_MIN } from "./logic/B06_Section_UI_Cross_Culvert_Const"; +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 { FILL_SLOPE_RATIO_MIN } from "./B06_Section_UI_Cross_Culvert_Const"; import type { CulvertDesignTrim, CulvertLayout, 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"; -import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall"; +} 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"; +import { buildRevetWallGeometry } from "./B06_Section_Cross_Wall_Geom"; type Role = "inlet" | "outlet"; diff --git a/B06_Section/logic/B06_Section_Cross_Design_Surface_Geom.ts b/B06_Section/logic/B06_Section_Cross_Design_Surface_Geom.ts new file mode 100644 index 000000000..68d037fac --- /dev/null +++ b/B06_Section/logic/B06_Section_Cross_Design_Surface_Geom.ts @@ -0,0 +1,71 @@ +/* ============================================================================= + * B06_Section_Cross_Design_Surface_Geom.ts + * 암선 · 포장층 **기하**(m 좌표) — 그리기는 `../B06_Section_UI_Cross_Design_Surface`(472-3). + * ========================================================================== */ + +import type { PavementEntry } from "@util/common_util_structure_pavement"; +import type { CrossDesign } from "../B06_Section_Api_Fetch"; + +export type Edge = { offset_m: number; elevation_m: number }; + +/** 암선 — 유효 지반 샘플을 `offsetM` 만큼 올린 꺾은선들(빈 · 무효 샘플에서 끊음 · 두 점 이상만). */ +export function rockBoundarySegments( + samples: Array<{ offset_m?: number; elevation_m?: number | null; valid: boolean }>, + offsetM: number, +): Edge[][] { + const segments: Edge[][] = []; + let current: Edge[] = []; + for (const sample of samples) { + const elevation = sample.elevation_m; + if (sample.valid === false || elevation === null || !Number.isFinite(elevation ?? NaN)) { + if (current.length > 1) segments.push(current); + current = []; + continue; + } + current.push({ offset_m: sample.offset_m ?? 0, elevation_m: (elevation as number) + offsetM }); + } + if (current.length > 1) segments.push(current); + return segments; +} + +/** 확폭 끈 구간 — 규격 폭 끝(좌 = left − 확폭좌 · 우 = right + 확폭우 · 표고는 두 끝 사이 직선). */ +function standardEdges(edges: { left: Edge; right: Edge }, design: CrossDesign) { + const { left, right } = edges; + const span = right.offset_m - left.offset_m || 1; + const at = (offset: number): Edge => ({ + offset_m: offset, + elevation_m: + left.elevation_m + ((right.elevation_m - left.elevation_m) * (offset - left.offset_m)) / span, + }); + return { + left: at(left.offset_m - (design.widening_left_m ?? 0)), + right: at(right.offset_m + (design.widening_right_m ?? 0)), + }; +} + +/** + * 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`section.pavement`)면 그 두께 · + * 확폭 켬/끔(여러 건은 아래로 차례로 쌓음) · 없으면 카드 포장 켬일 때 표준 두께. + * 층마다 네 꼭짓점(좌상 · 우상 · 우하 · 좌하). + */ +export function pavementLayers( + design: CrossDesign, + pavement?: readonly PavementEntry[] | null, +): Array<{ entry: PavementEntry | null; corners: Edge[] }> { + const edges = design.carriageway_edges ?? design.road_edges; + if (!edges) return []; + const layers = pavement?.length ? pavement : design.paved ? [null] : []; + let depth = 0; + return layers.map((entry) => { + const thickness = entry?.thickness_m ?? design.pavement_thickness_m ?? 0.2; + const { left, right } = entry && !entry.widening_on ? standardEdges(edges, design) : edges; + const corners = [ + { offset_m: left.offset_m, elevation_m: left.elevation_m - depth }, + { offset_m: right.offset_m, elevation_m: right.elevation_m - depth }, + { offset_m: right.offset_m, elevation_m: right.elevation_m - depth - thickness }, + { offset_m: left.offset_m, elevation_m: left.elevation_m - depth - thickness }, + ]; + depth += thickness; + return { entry, corners }; + }); +} diff --git a/B06_Section/logic/B06_Section_Cross_Excavation_Geom.ts b/B06_Section/logic/B06_Section_Cross_Excavation_Geom.ts new file mode 100644 index 000000000..da9e29022 --- /dev/null +++ b/B06_Section/logic/B06_Section_Cross_Excavation_Geom.ts @@ -0,0 +1,57 @@ +/* ============================================================================= + * B06_Section_Cross_Excavation_Geom.ts + * 벽 터파기 **기하** — 값은 `common_util_excavation` 한 곳(파이썬 짝 · 거울 시험). 그리기는 + * `../B06_Section_UI_Cross_Excavation`(472-3). + * ⚠ **비탈을 주지 않는다 — 수직.** 정본 식에 기울기 항이 없다. + * ========================================================================== */ + +import { + WALL_BLINDING_DEPTH_M, + WALL_BLINDING_WIDTH_M, + WALL_FOUNDATION_DEPTH_M, + WALL_FOUNDATION_WIDTH_M, + WALL_TRENCH_CLEARANCE_M, +} from "@util/common_util_excavation"; +import type { WallLayout } from "../B06_Section_UI_Cross_Culvert_Types"; + +/** 「기초유」인가 — 저장 제원 `foundation` 의 값. 비어 있으면 **그리지 않는다**(근거 없음). */ +export function foundationChoice(value: unknown): "기초유" | "기초버림" | null { + const text = typeof value === "string" ? value.trim() : ""; + if (text === "기초유") return "기초유"; + if (text === "기초버림") return "기초버림"; + return null; +} + +/** + * 벽 터파기 — 벽 바닥선 아래로 **수직**. 폭은 바닥 폭 + 여유 0.2, 깊이는 기초 몫. + * `foundation` 이 비어 있으면 null — 사용자가 고르기 전에는 근거가 없다. + */ +export function wallTrench( + wall: WallLayout, + foundation: "기초유" | "기초버림" | null, +): { + widthM: number; + depthM: number; + left: number; + right: number; + top: number; + bottom: number; +} | null { + if (foundation === null) return null; + const hasFoundation = foundation === "기초유"; + const depth = hasFoundation ? WALL_FOUNDATION_DEPTH_M : WALL_BLINDING_DEPTH_M; + // 폭: 기초 몫은 정본이 정한 값(0.9 / 0.7), 다만 벽 바닥이 그보다 넓으면 그 폭 + 여유. + const baseWidth = Math.abs(wall.bottomFront.offset - wall.bottomBack.offset); + const minWidth = hasFoundation ? WALL_FOUNDATION_WIDTH_M : WALL_BLINDING_WIDTH_M; + const width = Math.max(minWidth, baseWidth + (hasFoundation ? WALL_TRENCH_CLEARANCE_M : 0)); + const center = (wall.bottomFront.offset + wall.bottomBack.offset) / 2; + const top = Math.max(wall.bottomFront.elevation, wall.bottomBack.elevation); + return { + widthM: width, + depthM: depth, + left: center - width / 2, + right: center + width / 2, + top, + bottom: top - depth, + }; +} diff --git a/B06_Section/logic/B06_Section_Cross_Revetment_Geom.ts b/B06_Section/logic/B06_Section_Cross_Revetment_Geom.ts new file mode 100644 index 000000000..daa8a80f2 --- /dev/null +++ b/B06_Section/logic/B06_Section_Cross_Revetment_Geom.ts @@ -0,0 +1,334 @@ +/* ============================================================================= + * B06_Section_Cross_Revetment_Geom.ts + * 독립 기슭막이(배관과 무관) **기하** — 배관 세트 기슭막이와 **같은 배치·같은 산식·같은 도형**. + * 배관만 없을 뿐, 벽 자리·이동·한계는 배관 유출 벽과 한 몸으로 푼다(2026-08-28 사용자: + * "배관용에서 배관만 빼면 대부분 동일해야"). 그리기는 `../B06_Section_UI_Cross_Revetment`(472-3). + * + * · 자리(2026-08-29 사용자 개정): 조작은 **순수 좌우(x)·순수 상하(d)** 0.1m다. + * x = 자동 자리에서의 수평 이동, d = 노견에서 잰 수직 하강. 성토선 각도는 유지하고 + * **노견 확장 구간**이 길이를 흡수한다. 한계는 노견 안쪽 금지·확장 0(각도 유지)·매몰 금지. + * 배관 기준벽(`placePipeWall`)의 이동은 손대지 않는다. + * · 실제 적용값(한계 절삭 후)은 `appliedAdjust`로 돌려주고, 화면이 조작 저장소에 되받는다 + * — 눌러도 안 움직이는데 숫자만 커지는 것 방지(배관과 같은 syncApplied 규칙). + * · 다단: 배관과 **같은 함수** `buildExtrasAt`. 노견→벽 성토선은 designTrim의 slope. + * + * 3D 코리도는 `tiers[].polygon`·`span`을 그대로 스윕한다. + * ========================================================================== */ + +import type { CrossSection } from "../B06_Section_Api_Fetch"; +import { + FILL_SLOPE_RATIO_MIN, + PIPE_WALL_DEFAULT_RUN_M, + REVET_EMBED_DEPTH_M, + REVET_FILL_TOP_M, + WALL_MIN_PURE_HEIGHT_M, +} from "./B06_Section_UI_Cross_Culvert_Const"; +import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean"; +import { catalogRow } from "../../common_util/common_util_structure_catalog"; +import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const"; +import { fillWallBaseWidth, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve"; +import { buildExtrasAt } from "../B06_Section_UI_Cross_Culvert_Extra"; +import { + ZERO_ADJUST, + type CulvertDesignTrim, + type OutletFillSegment, + type WallAdjust, + type WallLayout, +} from "../B06_Section_UI_Cross_Culvert_Types"; +import { buildRevetWallGeometry } from "./B06_Section_Cross_Wall_Geom"; + +/** 서버가 얹어 주는 독립 기슭막이 제원(`section.revetment`, 구조물 정본 D군). */ +export interface RevetmentSpec { + structure_id: string | null; + type_id: string; + name: string; + start_m: number; + end_m: number; + anchor_m: number; + form?: string | null; + height_m?: number | null; + /** 사용자가 고른 설치 측 — "좌" | "우". */ + side?: string | null; + /** 기초 축 — "기초유" | "기초버림". **비어 있으면 터파기를 안 그린다**(근거 없음). */ + foundation?: string | null; + /** 단 수(다단). 1이면 단일 벽. */ + tiers?: number | null; + /** 추가 단(2단부터) 형태 · 높이 — 좌측 폼 「추가 단」 상자(178-3). 빈 값 = 본 벽 형태 · 기본 높이. */ + extras?: ReadonlyArray<{ form: string | null; height_m: number | null }> | null; + /** (구 모델) 기준 올림·좌우 이동 — 배관식 전환 뒤 자리는 조정창 4축(x·d)이 정한다. */ + lift_m?: number | null; + shift_m?: number | null; + /** 전면 기울기를 B08 과 같은 칸으로 가르는 값 — 성토/절토 · 전면 기울기(2026-09-14 B5). */ + face_role?: string | null; + face_slope_ratio?: number | string | null; +} + +/** 조정창 조작값 중 이 벽이 쓰는 축 — 배관 벽과 같은 형태(x 좌우·d 사면·h 높이). */ +export interface RevetmentAdjust { + x?: number; + d?: number | null; + h?: number | null; +} + +export interface RevetPoint { + offset: number; + elevation: number; +} + +/** 한 단(段) — 3D 스윕용 폴리곤(배관 벽과 같은 합성 단면). */ +export interface RevetmentTier { + polygon: RevetPoint[]; +} + +export interface RevetmentLayout { + side: "left" | "right"; + /** 1단 벽 높이(m) — **바닥~상단**. 조정창 높이 표시·조작의 기준값. */ + heightM: number; + /** 이 단면의 누가거리(m) — 부족 안내를 측점 단위로 센다(재렌더 중복 방지). */ + chainageM: number; + /** 벽 구조물 id — 그린 벽 신원표(323-5). */ + structureId: string | null; + /** 종류 이름(옹벽 · 흙막이 · 돌쌓기 …) — 툴팁 글(323-7 · 「독립 기슭막이」로 뜨던 것). */ + name: string; + /** 사용자가 요청한 단 수 — 실제로 선 단 수(`walls.length`)와 다르면 자리가 부족한 것이다. */ + requestedTiers: number; + /** 1단 벽 이음선 상단점(성토 설계선 트림 경계). */ + top: RevetPoint; + /** 벽 목록(1단 + 다단) — 배관 벽과 같은 `WallLayout`, 그리기가 그대로 쓴다. */ + walls: WallLayout[]; + /** 기초 축 — 저장 칸과 **같은 글자**(`foundation`). 터파기 그림이 이 값으로 갈린다. */ + foundation: string | null; + /** 벽 사이·벽 아래 성토부선(배관 다단과 같은 체계). */ + fillSegments: OutletFillSegment[]; + /** 성토 설계선을 벽 상단에서 끊고 노견→벽 성토선을 대신 그리는 트림. */ + designTrim: CulvertDesignTrim; + /** 한계 절삭 후 **실제 적용된** 1단 조작값 — 화면이 조작 저장소에 되받는다. */ + appliedAdjust: WallAdjust; + /** 3D 스윕용 단 목록 — 벽 폴리곤 그대로. */ + tiers: RevetmentTier[]; + /** 기준 측점 전/후 점유 길이(m) — 3D 스윕 범위. */ + span: { beforeM: number; afterM: number }; + /** 기준 측점이 아닌 측점(구간 연동) — 주 형상과 색을 갈라 그림(79-6). */ + linked: boolean; +} + +/** 형태(구조물 옵션) → 재질(높이 한계·라벨). 메 계열은 dry, 찰=wet, 콘크리트=concrete. */ +function materialOfForm(form?: string | null): RevetMaterial { + if (!form) return "dry"; + if (form.includes("찰")) return "wet"; + if (form.includes("콘크리트")) return "concrete"; + return "dry"; // 메쌓기·돌망태·통나무·바자 — 메 계열(2.0m 한계) +} + +/** 유효 지반 샘플의 바깥 한계 offset(설치 측). 없으면 null. */ +function sampleLimit(section: CrossSection, outward: number): number | null { + const offsets = section.samples + .filter((s) => s.valid !== false && typeof s.offset_m === "number") + .map((s) => s.offset_m as number); + if (!offsets.length) return null; + return outward > 0 ? Math.max(...offsets) : Math.min(...offsets); +} + +/** + * 노면 횡단물매를 **바깥 방향 1m당 표고 변화**로 돌려준다(하향이면 음수). + * 노면은 측구 쪽으로 내려가는 단일 평면이라(엔진 `road_z`), 노견을 밖으로 연장하면 + * 그 평면을 그대로 타고 가야 각도가 맞는다(2026-08-29 사용자). + * 1순위는 **실제 그려진 노견 구간**(차도 끝 → 노면 끝)의 기울기 — 부호를 추측하지 + * 않는다. 노견 폭이 0이면 `cross_slope_pct`를 측구 방향 부호로 쓴다. + */ +function roadSlopePerOutward( + design: NonNullable, + side: "left" | "right", + outward: number, +): number { + const edge = design.road_edges?.[side]; + const inner = design.carriageway_edges?.[side]; + if (edge && inner) { + const run = (edge.offset_m - inner.offset_m) * outward; + if (run > 1e-6) return (edge.elevation_m - inner.elevation_m) / run; + } + const pct = Number(design.cross_slope_pct); + if (!Number.isFinite(pct)) return 0; + return (pct / 100) * (design.ditch_side === side ? -1 : 1); +} + +/** + * 독립 기슭막이 단면 — 제원·높이·설계선이 없거나 자리를 못 찾으면 null. + * 1단 벽 = 배관 유출 벽과 같은 `placePipeWall`+`buildRevetWallGeometry`, + * 다단 = 배관과 같은 `buildExtrasAt`. + */ +export function computeRevetmentLayout( + section: CrossSection, + adjust?: RevetmentAdjust | null, +): RevetmentLayout | null { + const spec = section.revetment; + const design = section.design; + if (!spec || !design) return null; + const rawHeight = adjust?.h ?? spec.height_m; + // 빈 칸만 안 그림 — 0 이하는 아래 하한(0.1)에서 멈춤(54-51). + if (rawHeight == null || !Number.isFinite(Number(rawHeight))) return null; + const requestedHeight = Number(rawHeight); + + // 설치 측이 비어 있으면 **성토가 나는 쪽**에 세운다 — 좌측 「구조물 배치」의 C군 벽 + // (옹벽·돌쌓기 등)에는 설치 측 칸이 없기 때문이다(2026-09-06). 양측 절토면 벽이 설 + // 자리가 없으므로 그리지 않는다(면적도 그대로). + const fillSide = (): "left" | "right" | null => { + if (design.section_mode === "left_cut") return "right"; + if (design.section_mode === "right_cut") return "left"; + if (design.section_mode === "both_fill") return "left"; + return null; + }; + const side = spec.side === "우" ? "right" : spec.side === "좌" ? "left" : fillSide(); + if (!side) return null; + const outward = side === "left" ? 1 : -1; + const edge = design.road_edges?.[side]; + const groundAt = groundInterpolator(section.samples); + const limit = sampleLimit(section, outward); + if (!edge || !groundAt || limit === null) return null; + + // 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준). + // 형태별 높이 한계 없음 — 사용자 입력 그대로(하한만 둠). + const pureHeight = Math.max(requestedHeight, WALL_MIN_PURE_HEIGHT_M); + /** 기준선(근입 위)~상단 — 도형 계산은 종전대로 이 값으로 한다. */ + const height = pureHeight - REVET_EMBED_DEPTH_M; + + // 전면 기울기 — 관 흙막이와 같은 `outlineLeanOf`(형태 무관 · 형태는 안 무늬만). 단면유형 · + // 설치 측 · 높이 · 사용자 칸은 그대로 탐. 수량(B08)은 자기 칸으로 따로 셈. + const lean = outlineLeanOf({ + height_m: requestedHeight, + section_mode: design.section_mode, + side: spec.side, + face_role: spec.face_role, + face_slope_ratio: spec.face_slope_ratio, + }); + const roadSlope = roadSlopePerOutward(design, side, outward); + const autoOffset = + edge.offset_m + outward * (fillWallBaseWidth(height, lean) / 2 - REVET_FILL_TOP_M); + // x 원점 = 성토사면(1:1.2) 경사길이 4m 지점 — 저장된 x 의 절대 자리 그대로(79b). + const autoRun = PIPE_WALL_DEFAULT_RUN_M; + /** + * 조작 축(2026-08-29 사용자): x = **순수 좌우(m)**, d = **노견에서 잰 순수 수직 + * 하강(m)**. 성토선 각도는 그대로 두고 **노견 확장 구간**이 길이를 흡수한다 + * (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다). + */ + // 손대지 않은 벽(x · d 빈 칸) = 성토사면 제일 위(노견 · 79-5) · 손으로 넣은 벽은 종전 기본(79b). + const untouched = adjust?.d == null && !adjust?.x; + const defaultDrop = untouched ? 0 : autoRun / FILL_SLOPE_RATIO_MIN; + /** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리 — 음수 = 원점보다 + * 사면 위(노견 쪽)라 제일 위 자리는 x = −autoRun 이다. */ + const xFloor = (drop: number): number => FILL_SLOPE_RATIO_MIN * drop - autoRun; + const requestedX = untouched ? Number.NEGATIVE_INFINITY : (adjust?.x ?? 0); + // 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). 노면 위 금지 = d ≥ 0. + const appliedDrop = Math.round(Math.max(adjust?.d ?? defaultDrop, 0) * 10) / 10; + // 0.1 눈금 반올림(종전과 같음) · 반올림이 하한 아래로 내리면 하한(제일 위 자리가 노견 안쪽으로 안 감). + const floor = xFloor(appliedDrop); + const appliedX = Math.max(Math.round(Math.max(requestedX, floor) * 10) / 10, floor); + + const anchorOffset = autoOffset + outward * (autoRun + appliedX); + const baseElevation = edge.elevation_m - appliedDrop - height; + const floatGapM = Math.max(0, baseElevation - groundAt(anchorOffset)); + + const material = materialOfForm(spec.form); + const lengthM = Number.isFinite(spec.end_m - spec.start_m) ? spec.end_m - spec.start_m : null; + const tier1 = buildRevetWallGeometry({ + anchor: { offset: anchorOffset, elevation: baseElevation }, + outward, + height, + material, + role: "outlet", + form: spec.form, + lengthM, + floatGapM, + lean, + roadElevation: edge.elevation_m, + }); + // 넣은 좌우 이동이 **각도 하한**에 눌려 통째로 무시됐으면 그 하한을 남긴다(2026-09-07). + // 실측 — d 2.6m 자리에서 하한이 1.0m 를 넘어 「1.0m 를 넣어도 0.00m 이동」이 났음. + if (requestedX > 0.005 && floor > requestedX + 0.005) { + tier1.shiftFloorM = Math.round(floor * 100) / 100; + } + + // 다단 — 배관과 같은 함수. 1단 벽 전면 하단 꼭짓점을 성토부선 시작점으로. + const requestedTiers = Math.max(1, Math.round(Number(spec.tiers) || 1)); + const extras = + requestedTiers > 1 + ? buildExtrasAt(tier1.bottomFront, { + startBottomElevation: tier1.bottomBack.elevation, + outward, + groundAt, + limitOffset: limit, + // 단마다 상자 형태 · 높이(178-3) — 자리(x · d)는 다단 자동 규칙 그대로 + adjusts: Array.from({ length: requestedTiers - 1 }, (_, i) => ({ + ...ZERO_ADJUST, + m: spec.extras?.[i]?.form ?? null, + h: spec.extras?.[i]?.height_m ?? null, + })), + // 단들도 1단과 같은 형태로 선다 — 안 넘기면 형태가 비어 늘 메쌓기로 + // 그려졌다(2026-08-30 사용자: 형태별 모양이 누락되는 경우). + ownerForm: spec.form, + equalize: false, + leanFor: (form, pureHeightM) => + outlineLeanOf({ + form, + height_m: pureHeightM, + section_mode: design.section_mode, + side: spec.side, + }), + }) + : { walls: [], segments: [], appliedAdjusts: [], addable: false }; + + const walls: WallLayout[] = [tier1, ...extras.walls]; + // 물빼기 파이프 단면 = 표 형상칸.weep(322-1 · 옹벽) + if (catalogRow(spec.type_id)?.shapeOpts.weep) for (const wall of walls) wall.retaining = true; + const jt = tier1.fillTop; + // 노견 → (물매를 탄 노견 연장) → 벽 이음선 상단점. 연장 구간은 노면 물매 그대로 가고, + // 꺾임점에서 성토 물매 1:1.2로 벽 상단에 닿는다(2026-08-29 사용자 확정). + // D = 노견에서 벽 상단까지 바깥 방향 거리, R0 = 노견과 벽 상단의 낙차 + // (D − t) = 1.2 × (R0 + roadSlope × t) → t = (D − 1.2·R0) / (1 + 1.2·roadSlope) + const edgePoint = { offset: edge.offset_m, elevation: edge.elevation_m }; + const D = (jt.offset - edge.offset_m) * outward; + const R0 = edge.elevation_m - jt.elevation; + const denominator = 1 + FILL_SLOPE_RATIO_MIN * roadSlope; + const breakRun = + R0 > 1e-6 && D > 1e-6 && Math.abs(denominator) > 1e-9 + ? Math.min(D, Math.max(0, (D - FILL_SLOPE_RATIO_MIN * R0) / denominator)) + : 0; + const slope = { + points: + breakRun > 1e-6 + ? [ + edgePoint, + { + offset: edge.offset_m + outward * breakRun, + elevation: edge.elevation_m + roadSlope * breakRun, + }, + jt, + ] + : [edgePoint, jt], + }; + const designTrim: CulvertDesignTrim = + outward > 0 + ? { minOffset: -Infinity, maxOffset: jt.offset, maxElevation: jt.elevation, maxSlope: slope } + : { minOffset: jt.offset, maxOffset: Infinity, minElevation: jt.elevation, minSlope: slope }; + + return { + side, + heightM: pureHeight, + chainageM: Number(section.chainage_m) || 0, + structureId: spec.structure_id ?? null, + name: spec.name || "벽", + requestedTiers, + top: jt, + walls, + foundation: typeof spec.foundation === "string" ? spec.foundation.trim() || null : null, + fillSegments: extras.segments, + designTrim, + appliedAdjust: { x: appliedX, d: appliedDrop, h: adjust?.h ?? null, m: null }, + tiers: walls.map((wall) => ({ polygon: wall.points })), + span: { + beforeM: Math.max(spec.anchor_m - spec.start_m, 0), + afterM: Math.max(spec.end_m - spec.anchor_m, 0), + }, + linked: Math.abs((Number(section.chainage_m) || 0) - spec.anchor_m) > 0.02, + }; +} diff --git a/B06_Section/logic/B06_Section_Cross_Wall_Geom.ts b/B06_Section/logic/B06_Section_Cross_Wall_Geom.ts new file mode 100644 index 000000000..cf543dab1 --- /dev/null +++ b/B06_Section/logic/B06_Section_Cross_Wall_Geom.ts @@ -0,0 +1,89 @@ +/* ============================================================================= + * B06_Section_Cross_Wall_Geom.ts + * 기슭막이 벽 1매 **기하** — 배관 세트 벽(`_Cross_Culvert_Geom`/`_Culvert`)과 독립 기슭막이 + * (`_Cross_Revetment`)가 같은 산식을 쓴다. 그리기는 `../B06_Section_UI_Cross_Wall`(472-3). + * ========================================================================== */ + +import { + materialLabel, + REVET_EMBED_DEPTH_M, + REVET_LEAN_RATIO, + REVET_THICKNESS_M, + WALL_FILL_TOP_INSET_M, + WALL_MIN_PURE_HEIGHT_M, +} from "./B06_Section_UI_Cross_Culvert_Const"; +import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const"; +import type { OffsetPoint, WallLayout } from "../B06_Section_UI_Cross_Culvert_Types"; + +/** 벽 1매의 기하 입력 — 자리 기준은 **하단선 중점**(anchor.offset)과 그 자리 기준표고 + * (anchor.elevation = 관 invert 또는 지반). 벽은 그 위로 `height`만큼 선다. */ +export interface RevetWallGeometryInput { + /** 하단선 중점 offset + 기준표고(벽이 이 위로 선다). */ + anchor: OffsetPoint; + /** +1 = 화면 좌측(계류측), −1 = 우측. 전면 1:0.3이 이 부호로 기운다. */ + outward: number; + /** 계산용 높이(기준표고~상단, 근입 0.5 제외). */ + height: number; + material: RevetMaterial; + role: "inlet" | "outlet" | "extra"; + /** 표기용 형태 — 미지정이면 재질 라벨(메/찰/콘크리트). */ + form?: string | null; + lengthM?: number | null; + thickness?: number; + floatGapM?: number; + extraIndex?: number; + /** 전면 기울기 1:n 의 n — 표준경사(`leanOf`). 없으면 종전 0.3. */ + lean?: number; + /** 노면(계획고) — 벽 윗면이 이보다 높으면 여기서 멈춤. 높이 한도는 이것 하나. */ + roadElevation?: number | null; +} + +/** + * 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점을 만든다. 배관 벽(`buildWall`, + * `_Cross_Culvert_Geom` 335~376행)과 **같은 산식** — 배면 수직, 상단 변 = 사다리꼴 + * 상단(t/2)+띠(t), 전면 1:0.3, 하단선 = 기준표고 −0.5m 수평 기초. + */ +export function buildRevetWallGeometry(input: RevetWallGeometryInput): WallLayout { + const { anchor, outward, material, role } = input; + const road = input.roadElevation; + const height = + road != null && anchor.elevation + input.height > road + 1e-9 + ? Math.max(road - anchor.elevation, WALL_MIN_PURE_HEIGHT_M - REVET_EMBED_DEPTH_M) + : input.height; + const thickness = input.thickness ?? REVET_THICKNESS_M; + const lean = input.lean ?? REVET_LEAN_RATIO; + const baseWidth = thickness * 1.5 + lean * height; + const backOffset = anchor.offset - outward * (baseWidth / 2); + const topJoint = backOffset + outward * (thickness / 2); + const topElevation = anchor.elevation + height; + const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation }; + const topFront = topJoint + outward * thickness; + const frontXAt = (elevation: number): number => + topFront + outward * lean * (topElevation - elevation); + const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M; + const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation }; + const bottomFront: OffsetPoint = { + offset: frontXAt(bottomElevation), + elevation: bottomElevation, + }; + return { + role, + extraIndex: input.extraIndex, + form: input.form ?? materialLabel(material), + lengthM: input.lengthM ?? null, + backOffset, + outerOffset: bottomFront.offset, + base: anchor.elevation, + height, + floatGapM: input.floatGapM ?? 0, + material, + lean, + outward, + topBack, + topJoint: { offset: topJoint, elevation: topElevation }, + fillTop: { offset: topFront - outward * WALL_FILL_TOP_INSET_M, elevation: topElevation }, + bottomBack, + bottomFront, + points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront], + }; +} diff --git a/B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom.ts b/B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom.ts new file mode 100644 index 000000000..aa984a7f6 --- /dev/null +++ b/B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom.ts @@ -0,0 +1,223 @@ +/* ============================================================================= + * B06_Section_Cross_Wall_Hatch_Geom.ts + * 벽 안 무늬 · 물빼기 · 도수로 · 돌붙임 띠 **기하**(m 좌표) — 그리기는 + * `../B06_Section_UI_Cross_Wall_Hatch`(472-3). 횡단 그림과 B05 3D 띠 솔리드가 같은 것을 쓴다. + * ========================================================================== */ + +import { REVET_THICKNESS_M } from "./B06_Section_UI_Cross_Culvert_Const"; +import type { OffsetPoint, WallLayout } from "../B06_Section_UI_Cross_Culvert_Types"; + +/** 형태 문자열 → 표현 갈래. 판정 순서는 좁은 말(돌망태·바자)부터. */ +export type WallHatch = "dry" | "wet" | "concrete" | "gabion" | "log" | "wattle"; + +export function hatchOfForm(form: string | null | undefined): WallHatch { + if (!form) return "dry"; + if (form.includes("돌망태")) return "gabion"; + if (form.includes("통나무") || form.includes("목재")) return "log"; + if (form.includes("바자")) return "wattle"; + if (form.includes("콘크리트")) return "concrete"; + if (form.includes("찰")) return "wet"; + return "dry"; +} + +/** 켜 수 — 켜 높이가 목표치에 가장 가깝게, 최소 2켜. */ +export function courses(spanM: number, targetM: number): number { + return Math.max(2, Math.round(spanM / targetM)); +} + +/** 벽 전면 기운 띠(이음선~전면, 두께 REVET_THICKNESS_M)의 중심축 — 바닥 · 상단 offset · 바닥 표고 · 길이. */ +export function wallHatchBand(wall: WallLayout): { + centerBase: number; + centerTop: number; + embedBase: number; + spanM: number; +} { + // 하단선이 원지반을 따라 기울어 있으므로 띠 바닥도 하단선 위에서 잡는다. + const bottomSpan = wall.bottomFront.offset - wall.bottomBack.offset; + const bottomAtOffset = (offset: number): number => + Math.abs(bottomSpan) > 1e-9 + ? wall.bottomBack.elevation + + (wall.bottomFront.elevation - wall.bottomBack.elevation) * + ((offset - wall.bottomBack.offset) / bottomSpan) + : wall.bottomBack.elevation; + const centerBase = wall.outerOffset - wall.outward * (REVET_THICKNESS_M / 2); + const centerTop = wall.topJoint.offset + wall.outward * (REVET_THICKNESS_M / 2); + const embedBase = bottomAtOffset(centerBase); + const spanM = Math.max(0.3, wall.topBack.elevation - embedBase); + return { centerBase, centerTop, embedBase, spanM }; +} + +/** 물빼기 파이프 단면(73-2) — 보기용 고정값 · 지식DB 관경 · 간격과 별개. */ +const DRAIN_BAND_M = 1.5; +const DRAIN_DIAMETER_M = 0.15; +/** 앞쪽이 낮게 — 벽 두께 1m 당 낙차(보기용). */ +const DRAIN_FALL = 0.1; + +/** 직선 a→b 가 볼록 벽 단면 안을 지나는 구간 — clip 이 없는 곳(B07 오프스크린 수확)도 벽 밖으로 안 샘. */ +function insideRun(a: OffsetPoint, b: OffsetPoint, polygon: OffsetPoint[]): OffsetPoint[] | null { + const dx = b.offset - a.offset; + const dy = b.elevation - a.elevation; + const hits: number[] = []; + polygon.forEach((p, i) => { + const q = polygon[(i + 1) % polygon.length]; + const ex = q.offset - p.offset; + const ey = q.elevation - p.elevation; + const denominator = dx * ey - dy * ex; + if (Math.abs(denominator) < 1e-12) return; + const t = ((p.offset - a.offset) * ey - (p.elevation - a.elevation) * ex) / denominator; + const u = ((p.offset - a.offset) * dy - (p.elevation - a.elevation) * dx) / denominator; + if (u >= -1e-9 && u <= 1 + 1e-9) hits.push(t); + }); + if (hits.length < 2) return null; + const at = (t: number): OffsetPoint => ({ + offset: a.offset + dx * t, + elevation: a.elevation + dy * t, + }); + return [at(Math.min(...hits)), at(Math.max(...hits))]; +} + +/** + * 옹벽 물빼기 — 높이(바닥~상단)를 반올림(높이 / 1.5)개(최소 1) 띠로 고르게 나눠 띠마다 + * 가운데 하나. 배면 → 전면으로 꿰는 두 줄 평행선(앞쪽 조금 낮게) · 벽 두께만큼. + * 물빼기마다 벽 안 구간 목록(0~2 줄). + */ +export function wallDrainRuns(wall: WallLayout): OffsetPoint[][][] { + const bottom = wall.bottomBack.elevation; + const height = wall.topBack.elevation - bottom; + const count = Math.max(1, Math.round(height / DRAIN_BAND_M)); + const back = wall.backOffset; + const [, , topFront, bottomFront] = wall.points; + const drains: OffsetPoint[][][] = []; + for (let i = 0; i < count; i += 1) { + const center = bottom + (height * (i + 0.5)) / count; + // 그 높이의 전면 — 전면은 기울어 위로 갈수록 배면에 가까움. + const front = + bottomFront.offset + (topFront.offset - bottomFront.offset) * ((center - bottom) / height); + const fall = (DRAIN_FALL * Math.abs(front - back)) / 2; + const runs: OffsetPoint[][] = []; + for (const side of [-0.5, 0.5]) { + const level = center + side * DRAIN_DIAMETER_M; + const run = insideRun( + { offset: back, elevation: level + fall }, + { offset: front, elevation: level - fall }, + wall.points, + ); + if (run) runs.push(run); + } + drains.push(runs); + } + return drains; +} + +/** + * 도수로 · 돌붙임 띠 단면(90-2) — 윗면 = 사면선(겹친 점 뺌) · 변마다 아래쪽(사면 안쪽) 단위 법선 · + * 둘레 = 윗면 → 두께만큼 오프셋한 아랫면(거꾸로). 횡단 그림과 B05 3D 띠 솔리드가 같은 것을 쓴다. + */ +export function chuteBandOutline( + raw: OffsetPoint[], + thickness: number, +): { top: OffsetPoint[]; normals: OffsetPoint[]; outline: OffsetPoint[] } | null { + if (!(thickness > 0)) return null; + const top = raw.filter( + (p, i) => + i === 0 || + Math.hypot(p.offset - raw[i - 1].offset, p.elevation - raw[i - 1].elevation) > 1e-6, + ); + if (top.length < 2) return null; + // 변마다 아래쪽(사면 안쪽) 단위 법선 + const normals = top.slice(1).map((p, i) => { + const dx = p.offset - top[i].offset; + const dz = p.elevation - top[i].elevation; + const length = Math.hypot(dx, dz); + const sign = dx / length > 0 ? -1 : 1; + return { offset: (sign * -dz) / length, elevation: (sign * dx) / length }; + }); + // 꺾임은 두 오프셋 선의 교점(마이터 · 너무 뾰족하면 두 배까지) + const bottom = top.map((p, i) => { + const a = normals[Math.max(i - 1, 0)]; + const b = normals[Math.min(i, normals.length - 1)]; + const sum = { offset: a.offset + b.offset, elevation: a.elevation + b.elevation }; + const k = thickness / Math.max(sum.offset * a.offset + sum.elevation * a.elevation, 0.5); + return { offset: p.offset + sum.offset * k, elevation: p.elevation + sum.elevation * k }; + }); + return { top, normals, outline: [...top, ...bottom.reverse()] }; +} + +/** 꺾은선 앞에서부터 `lengthM` 까지(쓴 길이와 함께). */ +function trimPolyline( + points: readonly OffsetPoint[], + lengthM: number, +): { points: OffsetPoint[]; used: number } { + const out = points.slice(0, 1); + let used = 0; + for (let i = 1; i < points.length && lengthM - used > 1e-9; i += 1) { + const [a, b] = [points[i - 1], points[i]]; + const step = Math.hypot(b.offset - a.offset, b.elevation - a.elevation); + if (step < 1e-9) continue; + const t = Math.min((lengthM - used) / step, 1); + out.push({ + offset: a.offset + (b.offset - a.offset) * t, + elevation: a.elevation + (b.elevation - a.elevation) * t, + }); + used += step * t; + } + return { points: out, used }; +} + +/** 원지반 샘플 → 꺾은선(유효 점만 · 오프셋 차례). */ +export function groundLine( + samples: ReadonlyArray<{ + offset_m?: number | null; + elevation_m?: number | null; + valid?: boolean; + }>, +): OffsetPoint[] { + return samples + .filter((s) => s.valid !== false && s.offset_m != null && s.elevation_m != null) + .map((s) => ({ offset: s.offset_m as number, elevation: s.elevation_m as number })) + .sort((a, b) => a.offset - b.offset); +} + +/** + * 도수로 · 돌붙임 선 길이(294-4) — 선들을 차례로 사면 따라 `lengthM` 까지 자름 · 사면보다 길면 마지막 끝에서 + * 원지반(`ground`)을 바깥으로 따라 이어 그 길이까지 · 길이 없으면 그대로(원지반 닿으면 끝). 점 하나뿐인 선 + * (사면 없음 · 끝이 이미 원지반)은 길이가 있으면 원지반 따라 · 없으면 안 그림. 그림 전용(수량 · 내역 무관). + */ +export function chuteRun( + lines: readonly L[], + lengthM?: number | null, + ground?: readonly OffsetPoint[], +): L[] { + const drawn = (list: L[]) => list.filter((line) => line.points.length > 1); + if (!(lengthM && lengthM > 0) || !lines.length) return drawn([...lines]); + let left = lengthM; + const out: L[] = []; + for (const line of lines) { + if (left <= 1e-9) break; + const cut = trimPolyline(line.points, left); + out.push({ ...line, points: cut.points }); + left -= cut.used; + } + const last = out[out.length - 1]; + if (left > 1e-9 && ground?.length && last) { + const first = lines[0].points[0]; + const end = last.points[last.points.length - 1]; + const outward = Math.sign(end.offset - first.offset) || Math.sign(end.offset) || 1; + const along = [ + end, + ...ground + .filter((p) => (p.offset - end.offset) * outward > 1e-6) + .sort((a, b) => (a.offset - b.offset) * outward), + ]; + out.push({ ...last, points: trimPolyline(along, left).points }); + } + return drawn(out); +} + +/** 띠가 서는 유출 조각(152-3) — 성토 조각 + 내림 · 수평 절토 조각(3도 절토선 · 끝 ≤ 시작). + * 3도 선이 못 만나 복합경사로 오르는 절토 조각은 뺌. B06 관 · 흙막이 띠와 3D 띠가 같은 거름. */ +export function isChuteSegment(segment: { kind?: string; points: OffsetPoint[] }): boolean { + if (segment.kind !== "cut") return true; + const { points } = segment; + return points.length > 1 && points[points.length - 1].elevation <= points[0].elevation + 1e-6; +} diff --git a/B06_Section/B06_Section_Route_Carry_Patch.ts b/B06_Section/logic/B06_Section_Route_Carry_Patch.ts similarity index 95% rename from B06_Section/B06_Section_Route_Carry_Patch.ts rename to B06_Section/logic/B06_Section_Route_Carry_Patch.ts index e365281ef..50ac352de 100644 --- a/B06_Section/B06_Section_Route_Carry_Patch.ts +++ b/B06_Section/logic/B06_Section_Route_Carry_Patch.ts @@ -8,26 +8,26 @@ * · `routeCarryPatches` — 노선 바뀐 [저장]: 옛 노선 세션 칸 → 새 노선 서버 상세(측점 열쇠 넘김 * `logic/B06_Section_Route_Carry`) → 얹기 → patch(옛 브라우저 길 `carryAfterRouteSave` 와 같은 차례) · * 서버 Node 입구 `stations.carry` 가 부름(`A00_Common/design/logic/design_calc_node_carry.ts`). - * logic 밖 = 구조물 그림 기하(관 벽 · 단 · 구간 · 세월 · BOX 기본값)를 끔 — 서버 Node 묶음 알려진 끌림. + * 관 벽 · 단 · 구간 · 세월 · BOX 기본값 = 구조물 기하 파일(`_Cross_Culvert_Types` · `_Wire` · `_Ford_Geom` · `_Box_Geom`). * ========================================================================== */ import type { CrossDesign, 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 { 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"; +} 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 { PIPE_MATCH_M } from "../../common_util/common_util_pipe_match"; +import { inferStationInterval } from "../../common_util/common_util_station"; +import { applySpanPatch, WALL_MIN_PURE_HEIGHT_M } from "./B06_Section_UI_Cross_Culvert_Const"; +import { tierSpanOf } from "../B06_Section_UI_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"; -import { STRUCTURE_ADJUST_KEYS, USER_TOUCHED_KEYS } from "./logic/B06_Section_Design_Keys"; +import { ROW_CARRY_KEYS } from "./B06_Section_Station_Swap"; +import { mergeCrossPatches } from "./B06_Section_UI_Page_Patches"; +import { STRUCTURE_ADJUST_KEYS, USER_TOUCHED_KEYS } from "./B06_Section_Design_Keys"; import { carryChainageStates, carryKeyedStates, @@ -37,7 +37,7 @@ import { type KeyedState, type Owner, type Raw, -} from "./logic/B06_Section_Route_Carry"; +} from "./B06_Section_Route_Carry"; export type Cells = Record; /** 누가거리(숫자) → 쪽마다 값(세션에 있는 쪽만 · 노견 확장 · 소단 깊이). */ diff --git a/B06_Section/B06_Section_Structure_Layouts.ts b/B06_Section/logic/B06_Section_Structure_Layouts.ts similarity index 94% rename from B06_Section/B06_Section_Structure_Layouts.ts rename to B06_Section/logic/B06_Section_Structure_Layouts.ts index abe2571dc..49f4d3e0d 100644 --- a/B06_Section/B06_Section_Structure_Layouts.ts +++ b/B06_Section/logic/B06_Section_Structure_Layouts.ts @@ -11,28 +11,28 @@ * ========================================================================== */ 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 { 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 type { CrossSection } from "../B06_Section_Api_Fetch"; +import { withoutGuardedFill } from "./B06_Section_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 { REVET_EMBED_DEPTH_M } from "./B06_Section_UI_Cross_Culvert_Const"; +import { groundInterpolator } from "./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 { computeCardCulvert, culvertLinkFor, ownWallsDrawAt, tierSpanOf, -} from "./B06_Section_UI_Cross_Culvert_Wire"; +} from "../B06_Section_UI_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"; -import { computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment"; +} 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"; +import { computeRevetmentLayout } from "./B06_Section_Cross_Revetment_Geom"; /** 같은 측점으로 볼 누가거리 오차(m) — 정본 반올림 자릿수보다 크게 잡는다. */ const CHAINAGE_TOLERANCE_M = 0.02; diff --git a/B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts b/B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts index 0af68f0fe..23f3dbb4e 100644 --- a/B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts +++ b/B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts @@ -12,13 +12,13 @@ import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; import { computeStoredLayouts as computeLayouts, type StoredLayouts, -} from "../B06_Section/B06_Section_Structure_Layouts"; +} from "../B06_Section/logic/B06_Section_Structure_Layouts"; import { appendBoxOverlay } from "../B06_Section/B06_Section_UI_Cross_Box"; import { appendCulvertOverlay } from "../B06_Section/B06_Section_UI_Cross_Culvert"; import { appendFordOverlay } from "../B06_Section/B06_Section_UI_Cross_Ford"; import { appendFordPavementOverlay } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement"; import { appendUnderdrainOverlay } from "../B06_Section/B06_Section_UI_Cross_Underdrain"; -import { groundLine } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch"; +import { groundLine } from "../B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom"; import { appendCrossDesignOverlay, appendPavementOverlay, diff --git a/common_util/common_util_station.ts b/common_util/common_util_station.ts index 471d77ad7..5cd2cd085 100644 --- a/common_util/common_util_station.ts +++ b/common_util/common_util_station.ts @@ -29,3 +29,19 @@ export function stationPartsLabel( const scale = decimals ? 10 : 1; return `${stationNumber}+${Math.round(remainder * scale) / scale}`; } + +/** 측점 간격을 실제 측점 배열에서 되짚는다(최빈 간격). 라벨 표기의 분모다. */ +export function inferStationInterval(stations: Array<{ chainage_m: number }>): number { + const counts = new Map(); + for (let index = 1; index < stations.length; index += 1) { + const difference = stations[index].chainage_m - stations[index - 1].chainage_m; + if (difference <= 0) continue; + const rounded = Math.round(difference * 10) / 10; + counts.set(rounded, (counts.get(rounded) ?? 0) + 1); + } + return ( + [...counts.entries()].sort( + ([intervalA, countA], [intervalB, countB]) => countB - countA || intervalB - intervalA, + )[0]?.[0] ?? 1 + ); +} diff --git a/common_util/common_util_svg.ts b/common_util/common_util_svg.ts index e03e9e6c5..47d9d8afe 100644 --- a/common_util/common_util_svg.ts +++ b/common_util/common_util_svg.ts @@ -33,20 +33,4 @@ export function svgText( return text; } -/** 측점 간격을 실제 측점 배열에서 되짚는다(최빈 간격). 라벨 표기의 분모다. */ -export function inferStationInterval(stations: Array<{ chainage_m: number }>): number { - const counts = new Map(); - for (let index = 1; index < stations.length; index += 1) { - const difference = stations[index].chainage_m - stations[index - 1].chainage_m; - if (difference <= 0) continue; - const rounded = Math.round(difference * 10) / 10; - counts.set(rounded, (counts.get(rounded) ?? 0) + 1); - } - return ( - [...counts.entries()].sort( - ([intervalA, countA], [intervalB, countB]) => countB - countA || intervalB - intervalA, - )[0]?.[0] ?? 1 - ); -} - -export { stationLabel } from "./common_util_station"; +export { inferStationInterval, stationLabel } from "./common_util_station"; diff --git a/resources/tester/helper_107_ford_free.cjs b/resources/tester/helper_107_ford_free.cjs index e780fdded..04987ce05 100644 --- a/resources/tester/helper_107_ford_free.cjs +++ b/resources/tester/helper_107_ford_free.cjs @@ -54,7 +54,7 @@ bodies.box.control.select(352.14, "left"); out.released = released; // 107-2 — 원지반을 바꾼 사본: 세월교 바닥 = 노면 − (관경 + 토피) · 박스 본체 · 끝 · 날개 · 흙막이 자리 그대로 -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts")); const fixture = JSON.parse( fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), ); diff --git a/resources/tester/helper_118_pipe_pick.cjs b/resources/tester/helper_118_pipe_pick.cjs index 6b38ffe6a..b9c94c48d 100644 --- a/resources/tester/helper_118_pipe_pick.cjs +++ b/resources/tester/helper_118_pipe_pick.cjs @@ -120,7 +120,7 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts")); const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert")); const { wallStandsAt } = loadTs(B06("B06_Section_UI_Cross_Culvert_Wire")); const { applyStructurePick } = loadTs(B06("B06_Section_UI_Page_Structure_Pick")); diff --git a/resources/tester/helper_135_3_chute_solid.cjs b/resources/tester/helper_135_3_chute_solid.cjs index 5cb934254..0d7512e0b 100644 --- a/resources/tester/helper_135_3_chute_solid.cjs +++ b/resources/tester/helper_135_3_chute_solid.cjs @@ -32,7 +32,7 @@ const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); const { buildCorridorStructures } = loadTs(B05("B05_Profile_UI_Corridor_Structures")); 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 { chuteBandOutline, isChuteSegment } = loadTs(B06("logic/B06_Section_Cross_Wall_Hatch_Geom")); const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom")); const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford")); const { outletFillLine } = loadTs(B06("B06_Section_UI_Cross_Ford_Pavement")); diff --git a/resources/tester/helper_137_design_pick.cjs b/resources/tester/helper_137_design_pick.cjs index 208aa4da8..6c912dbe0 100644 --- a/resources/tester/helper_137_design_pick.cjs +++ b/resources/tester/helper_137_design_pick.cjs @@ -219,7 +219,7 @@ out.zone = { }; // ⑤ 실제 측점 — 설계선 + 배관 오버레이를 그린 뒤 고름을 붙임(카드와 같은 차례) -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts")); const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert")); const { appendCrossDesignOverlay } = loadTs(B06("B06_Section_UI_Cross_Design")); const { raiseWalls } = loadTs(B06("B06_Section_UI_Cross_Wall")); diff --git a/resources/tester/helper_139_4_buried_outlet.cjs b/resources/tester/helper_139_4_buried_outlet.cjs index 98e709a4b..20547899d 100644 --- a/resources/tester/helper_139_4_buried_outlet.cjs +++ b/resources/tester/helper_139_4_buried_outlet.cjs @@ -29,7 +29,7 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts")); const { computeStructureAreas } = loadTs( path.join(ROOT, "common_util", "common_util_cross_structure_areas.ts"), ); diff --git a/resources/tester/helper_152_2_link_height.cjs b/resources/tester/helper_152_2_link_height.cjs index b36bb3b4c..80e09e636 100644 --- a/resources/tester/helper_152_2_link_height.cjs +++ b/resources/tester/helper_152_2_link_height.cjs @@ -30,7 +30,7 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", f); -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts")); const fixture = JSON.parse( fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), diff --git a/resources/tester/helper_152_3_chute_cut.cjs b/resources/tester/helper_152_3_chute_cut.cjs index 2981239e8..f921e1666 100644 --- a/resources/tester/helper_152_3_chute_cut.cjs +++ b/resources/tester/helper_152_3_chute_cut.cjs @@ -29,8 +29,8 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts")); -const { isChuteSegment, chuteBandOutline } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts")); +const { isChuteSegment, chuteBandOutline } = loadTs(B06("logic/B06_Section_Cross_Wall_Hatch_Geom")); const { chuteBands } = loadTs( path.join(ROOT, "B05_Profile", "B05_Profile_UI_Corridor_Structures_Chute.ts"), ); diff --git a/resources/tester/helper_152_4_chute_pick.cjs b/resources/tester/helper_152_4_chute_pick.cjs index 3ac79ab80..eb69336c7 100644 --- a/resources/tester/helper_152_4_chute_pick.cjs +++ b/resources/tester/helper_152_4_chute_pick.cjs @@ -125,7 +125,7 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts")); const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert")); const { attachChuteBandPick } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch")); const { applyStructurePick } = loadTs(B06("B06_Section_UI_Page_Structure_Pick")); diff --git a/resources/tester/helper_152_5_pavement_pick.cjs b/resources/tester/helper_152_5_pavement_pick.cjs index 857c34a2f..576092620 100644 --- a/resources/tester/helper_152_5_pavement_pick.cjs +++ b/resources/tester/helper_152_5_pavement_pick.cjs @@ -47,7 +47,11 @@ function loadTs(file, stubs = {}) { }).loadTs(file); } -const surface = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Design_Surface.ts")); +const surface = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Design_Surface.ts"), { + "./logic/B06_Section_Cross_Design_Surface_Geom": loadTs( + path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Design_Surface_Geom.ts"), + ), +}); const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), { "../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6) "../b_page_state": { readState: () => null, writeState: () => {} }, diff --git a/resources/tester/helper_178_3_slope_tiers.cjs b/resources/tester/helper_178_3_slope_tiers.cjs index de8e4164f..1e6d8d6ff 100644 --- a/resources/tester/helper_178_3_slope_tiers.cjs +++ b/resources/tester/helper_178_3_slope_tiers.cjs @@ -35,7 +35,7 @@ const { ADDON_SPECS } = loadTs(design("logic/design_structure_slope_tier.ts")); const { STRUCTURE_SPECS } = loadTs(design("logic/design_structure_pipe.ts")); const { wallSpecsFrom } = loadTs(path.join(ROOT, "common_util", "common_util_structure_walls.ts")); const { computeRevetmentLayout } = loadTs( - path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Revetment.ts"), + path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Revetment_Geom.ts"), ); const r3 = (v) => Math.round(v * 1000) / 1000; diff --git a/resources/tester/helper_186_1_outlet_fold.cjs b/resources/tester/helper_186_1_outlet_fold.cjs index 0b754a8b7..a42493be7 100644 --- a/resources/tester/helper_186_1_outlet_fold.cjs +++ b/resources/tester/helper_186_1_outlet_fold.cjs @@ -30,7 +30,7 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", f); -const { computeStoredLayouts, structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts.ts")); +const { computeStoredLayouts, structureAreaRows } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts")); const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts")); const read = (name) => JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", name), "utf8")).cross_sections; diff --git a/resources/tester/helper_190_1_basin_pick.cjs b/resources/tester/helper_190_1_basin_pick.cjs index 2206c2fae..3aac4ae24 100644 --- a/resources/tester/helper_190_1_basin_pick.cjs +++ b/resources/tester/helper_190_1_basin_pick.cjs @@ -80,7 +80,7 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts")); const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert")); const { outlineBox } = loadTs(B06("B06_Section_UI_Cross_Culvert_Types")); diff --git a/resources/tester/helper_280_3_route_undo_facilities.cjs b/resources/tester/helper_280_3_route_undo_facilities.cjs index 74fc8bb1c..c964b4727 100644 --- a/resources/tester/helper_280_3_route_undo_facilities.cjs +++ b/resources/tester/helper_280_3_route_undo_facilities.cjs @@ -223,7 +223,7 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts"); const storeMod = load("A00_Common", "design", "design_store.ts"); const sectionStore = load("B06_Section", "B06_Section_Section_Store.ts"); const common = load("B06_Section", "B06_Section_UI_Section_Common.ts"); -const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts"); +const layouts = load("B06_Section", "logic", "B06_Section_Structure_Layouts.ts"); const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts"); const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts"); const baseMod = load("A00_Common", "design", "design_profile_base.ts"); diff --git a/resources/tester/helper_286_1_ford_pavement.cjs b/resources/tester/helper_286_1_ford_pavement.cjs index af326b295..07a7301b9 100644 --- a/resources/tester/helper_286_1_ford_pavement.cjs +++ b/resources/tester/helper_286_1_ford_pavement.cjs @@ -23,11 +23,20 @@ function loadTs(file, stubs = {}) { const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); const station = loadTs(path.join(ROOT, "common_util", "common_util_structure_station.ts")); +// 띠 기하(472-3 logic) — 그림 파일 · 물넘이포장이 같은 모듈 +const hatchGeom = loadTs( + path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Wall_Hatch_Geom.ts"), + { "./B06_Section_UI_Cross_Culvert_Const": {} }, +); const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"), { "./B06_Section_UI_Cross_Wall_Hatch": loadTs( path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"), - { "./logic/B06_Section_UI_Cross_Culvert_Const": {} }, + { + "./logic/B06_Section_UI_Cross_Culvert_Const": {}, + "./logic/B06_Section_Cross_Wall_Hatch_Geom": hatchGeom, + }, ), + "./logic/B06_Section_Cross_Wall_Hatch_Geom": hatchGeom, "./B06_Section_UI_Cross_Design_Surface": { appendPavementOverlay() {} }, // 관 신원표(323-5 · 472-2 에서 common_util 로) — 진짜 · 장부 안 꽂음 = 옛 자리 이름 "../common_util/common_util_pick_tag": loadTs( diff --git a/resources/tester/helper_294_3_ford_extend_chute.cjs b/resources/tester/helper_294_3_ford_extend_chute.cjs index d4a04dc54..d61cdb918 100644 --- a/resources/tester/helper_294_3_ford_extend_chute.cjs +++ b/resources/tester/helper_294_3_ford_extend_chute.cjs @@ -56,7 +56,7 @@ pave.appendFordPavementOverlay( const paveLayer = svg.children.find((k) => (k.attrs.class ?? "").includes("ford-pavement")); // 294-4 도수로 · 돌붙임 끝단 · 길이 -const hatch = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts")); +const hatch = load(path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Wall_Hatch_Geom.ts")); const P = (offset, elevation) => ({ offset, elevation }); const slope = [{ points: [P(0, 0), P(3, -4)], kind: "fill" }]; // 사면 5 m const flat = [P(3, -4), P(5, -4), P(8, -4)]; // 사면 끝 바깥 원지반 diff --git a/resources/tester/helper_315_ford_chute_overflow.cjs b/resources/tester/helper_315_ford_chute_overflow.cjs index f32a65852..90b3a6163 100644 --- a/resources/tester/helper_315_ford_chute_overflow.cjs +++ b/resources/tester/helper_315_ford_chute_overflow.cjs @@ -44,7 +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 hatch = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts")); +const hatch = load(path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Wall_Hatch_Geom.ts")); const fillGround = hatch.groundLine(input.fillSamples); const dipGround = hatch.groundLine(input.dipSamples); const chute = (design, ground, length) => diff --git a/resources/tester/helper_318_cross_card.cjs b/resources/tester/helper_318_cross_card.cjs index c574bec62..870aa8fc2 100644 --- a/resources/tester/helper_318_cross_card.cjs +++ b/resources/tester/helper_318_cross_card.cjs @@ -592,10 +592,16 @@ const loadDrafts = makeLoader( ); // ── 6. B05 [저장](캐시 patch) — 종류 바꾼 측점 = structure_reset · 옛 조정값 안 실음 ───── +const DESIGN_KEYS = { + USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"], + STRUCTURE_ADJUST_KEYS: makeLoader([B06("logic/B06_Section_Design_Keys")])( + B06("logic/B06_Section_Design_Keys"), + ).STRUCTURE_ADJUST_KEYS, +}; const loadStore = makeLoader( [ B06("B06_Section_Section_Store"), - B06("B06_Section_Route_Carry_Patch"), // patch 바탕 한 벌(472-6) + B06("logic/B06_Section_Route_Carry_Patch"), // patch 바탕 한 벌(472-6) B06("B06_Section_Api_Culvert_Options"), path.join(ROOT, "A00_Common", "b_page_state.ts"), path.join(ROOT, "A00_Common", "design", "design_selection.ts"), @@ -605,12 +611,9 @@ const loadStore = makeLoader( "./B06_Section_Cross_Refresh": { USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"], }, - "./logic/B06_Section_Design_Keys": { - USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"], - STRUCTURE_ADJUST_KEYS: makeLoader([B06("logic/B06_Section_Design_Keys")])( - B06("logic/B06_Section_Design_Keys"), - ).STRUCTURE_ADJUST_KEYS, - }, + "./logic/B06_Section_Design_Keys": DESIGN_KEYS, + // patch 바탕(logic/Route_Carry_Patch · 472-3)이 부르는 이름 + "./B06_Section_Design_Keys": DESIGN_KEYS, }, ); diff --git a/resources/tester/helper_322_0_golden.cjs b/resources/tester/helper_322_0_golden.cjs index c8a78b601..bfbacc033 100644 --- a/resources/tester/helper_322_0_golden.cjs +++ b/resources/tester/helper_322_0_golden.cjs @@ -118,7 +118,7 @@ const STUBS = new Map([ { kindChangedChainages: () => [], STRUCTURE_ADJUST_KEYS: [] }, ], [ - B06("B06_Section_Structure_Layouts"), + B06("logic/B06_Section_Structure_Layouts"), { applyStructureAreaRows: () => {}, structureAreaRows: () => [] }, ], [ @@ -212,7 +212,7 @@ const { const { loadSectionDetail } = loadTs(B06("B06_Section_Section_Store")); const { forgetDraftStations } = loadTs(B06("B06_Section_Section_Drafts")); const { buildCorridorStructures } = loadTs(B05("B05_Profile_UI_Corridor_Structures")); -const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment")); +const { computeRevetmentLayout } = loadTs(B06("logic/B06_Section_Cross_Revetment_Geom")); const { buildStructureLane } = loadTs(B05("B05_Profile_UI_Structures_Marks")); const { pickGroups } = loadTs(B05("B05_Profile_UI_Structures_Panel_Types")); const { structureDisplayName } = loadTs(B06("B06_Section_UI_Section_Common")); diff --git a/resources/tester/helper_349_0o_redo_same.cjs b/resources/tester/helper_349_0o_redo_same.cjs index b95cd24e7..eb80ee2cd 100644 --- a/resources/tester/helper_349_0o_redo_same.cjs +++ b/resources/tester/helper_349_0o_redo_same.cjs @@ -193,7 +193,7 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts"); const storeMod = load("A00_Common", "design", "design_store.ts"); const sectionStore = load("B06_Section", "B06_Section_Section_Store.ts"); const common = load("B06_Section", "B06_Section_UI_Section_Common.ts"); -const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts"); +const layouts = load("B06_Section", "logic", "B06_Section_Structure_Layouts.ts"); const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts"); const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts"); const baseMod = load("A00_Common", "design", "design_profile_base.ts"); diff --git a/resources/tester/helper_349_0q_undo_echo.cjs b/resources/tester/helper_349_0q_undo_echo.cjs index 9146264e3..78a6ad618 100644 --- a/resources/tester/helper_349_0q_undo_echo.cjs +++ b/resources/tester/helper_349_0q_undo_echo.cjs @@ -231,7 +231,7 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts"); const storeMod = load("A00_Common", "design", "design_store.ts"); const sectionStore = load("B06_Section", "B06_Section_Section_Store.ts"); const common = load("B06_Section", "B06_Section_UI_Section_Common.ts"); -const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts"); +const layouts = load("B06_Section", "logic", "B06_Section_Structure_Layouts.ts"); const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts"); const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts"); const baseMod = load("A00_Common", "design", "design_profile_base.ts"); diff --git a/resources/tester/helper_356_4_undo_grade.cjs b/resources/tester/helper_356_4_undo_grade.cjs index 96f614fcf..c031f46cb 100644 --- a/resources/tester/helper_356_4_undo_grade.cjs +++ b/resources/tester/helper_356_4_undo_grade.cjs @@ -224,7 +224,7 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts"); const storeMod = load("A00_Common", "design", "design_store.ts"); const sectionStore = load("B06_Section", "B06_Section_Section_Store.ts"); const common = load("B06_Section", "B06_Section_UI_Section_Common.ts"); -const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts"); +const layouts = load("B06_Section", "logic", "B06_Section_Structure_Layouts.ts"); const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts"); const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts"); const baseMod = load("A00_Common", "design", "design_profile_base.ts"); diff --git a/resources/tester/helper_384_4_adjusts.cjs b/resources/tester/helper_384_4_adjusts.cjs index 5ae01db00..a25f2b152 100644 --- a/resources/tester/helper_384_4_adjusts.cjs +++ b/resources/tester/helper_384_4_adjusts.cjs @@ -42,7 +42,7 @@ function memoryStorage() { globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() }; const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); -const { structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts")); +const { structureAreaRows } = loadTs(B06("logic/B06_Section_Structure_Layouts")); const { withDraftAdjusts } = loadTs(B06("B06_Section_Section_Adjusts")); const { writeState } = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts")); diff --git a/resources/tester/helper_387_4b_pipe_options.cjs b/resources/tester/helper_387_4b_pipe_options.cjs index af8cf6a80..342b1b105 100644 --- a/resources/tester/helper_387_4b_pipe_options.cjs +++ b/resources/tester/helper_387_4b_pipe_options.cjs @@ -43,7 +43,7 @@ function memoryStorage() { globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() }; const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); -const { structureAreaRows, computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts")); +const { structureAreaRows, computeStoredLayouts } = loadTs(B06("logic/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")); diff --git a/resources/tester/helper_428_pipe_move_entry.ts b/resources/tester/helper_428_pipe_move_entry.ts index 4a36f9bd0..59f39c585 100644 --- a/resources/tester/helper_428_pipe_move_entry.ts +++ b/resources/tester/helper_428_pipe_move_entry.ts @@ -5,7 +5,7 @@ export { applyStructureAreaRows, computeStoredLayouts, structureAreaRows, -} from "../../B06_Section/B06_Section_Structure_Layouts"; +} from "../../B06_Section/logic/B06_Section_Structure_Layouts"; export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel"; export { writeState } from "../../A00_Common/b_page_state"; export { rebasePipeDraft } from "../../common_util/common_util_route_follow"; diff --git a/resources/tester/helper_445_save_patch.cjs b/resources/tester/helper_445_save_patch.cjs index 6ebdeb7f5..b3467caab 100644 --- a/resources/tester/helper_445_save_patch.cjs +++ b/resources/tester/helper_445_save_patch.cjs @@ -76,7 +76,7 @@ const load = makeLoader( B06("logic/B06_Section_UI_Page_Patches"), B06("logic/B06_Section_Design_Keys"), // 칸 목록 · 얹기 · patch 꼴 한 벌(472-6) B06("logic/B06_Section_Route_Carry"), - B06("B06_Section_Route_Carry_Patch"), + B06("logic/B06_Section_Route_Carry_Patch"), path.join(ROOT, "common_util", "common_util_pipe_match.ts"), path.join(ROOT, "A00_Common", "b_page_state.ts"), path.join(ROOT, "A00_Common", "design", "design_selection.ts"), diff --git a/resources/tester/helper_472_1_browser_entry.ts b/resources/tester/helper_472_1_browser_entry.ts index c367bec69..fe5c74b9f 100644 --- a/resources/tester/helper_472_1_browser_entry.ts +++ b/resources/tester/helper_472_1_browser_entry.ts @@ -29,7 +29,7 @@ export { createCrossStage } from "../../B06_Section/logic/B06_Section_Cross_Stag export { applyStructureAreaRows, structureAreaRows, -} from "../../B06_Section/B06_Section_Structure_Layouts"; +} from "../../B06_Section/logic/B06_Section_Structure_Layouts"; export { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist"; export { parseDtmGrid } from "../../common_util/common_util_surface_sampler"; export { computeMassHaul } from "../../common_util/common_util_mass_haul"; diff --git a/resources/tester/helper_472_6_route_entry.ts b/resources/tester/helper_472_6_route_entry.ts index c78f04b57..1c6a77528 100644 --- a/resources/tester/helper_472_6_route_entry.ts +++ b/resources/tester/helper_472_6_route_entry.ts @@ -20,7 +20,7 @@ export { withCarriedEdits, withDraftAdjusts, } from "../../B06_Section/B06_Section_Section_Adjusts"; -export { crossPatchesFromCache } from "../../B06_Section/B06_Section_Route_Carry_Patch"; +export { crossPatchesFromCache } from "../../B06_Section/logic/B06_Section_Route_Carry_Patch"; export { mergeCrossPatches } from "../../B06_Section/logic/B06_Section_UI_Page_Patches"; export { kindChangedChainages } from "../../B06_Section/B06_Section_Api_Culvert_Options"; export { pipeChainageOf } from "../../A00_Common/design/design_pipe_ids"; diff --git a/resources/tester/helper_73_4_body_guard.cjs b/resources/tester/helper_73_4_body_guard.cjs index ba0734a27..cea3b5c0e 100644 --- a/resources/tester/helper_73_4_body_guard.cjs +++ b/resources/tester/helper_73_4_body_guard.cjs @@ -31,9 +31,9 @@ const resolve = (base) => const B06 = (f) => path.join(ROOT, "B06_Section", f); const { computeStoredLayouts, structureAreaRows, trimOfLayouts } = loadTs( - B06("B06_Section_Structure_Layouts.ts"), + B06("logic/B06_Section_Structure_Layouts.ts"), ); -const { bodyGuardBase } = loadTs(B06("B06_Section_UI_Cross_Body_Guard.ts")); +const { bodyGuardBase } = loadTs(B06("logic/B06_Section_Cross_Body_Guard.ts")); const fixture = JSON.parse( fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), diff --git a/resources/tester/helper_79_guard_shape.cjs b/resources/tester/helper_79_guard_shape.cjs index b0b53c880..30fba8dde 100644 --- a/resources/tester/helper_79_guard_shape.cjs +++ b/resources/tester/helper_79_guard_shape.cjs @@ -29,11 +29,11 @@ const resolve = (base) => ) ?? `${base}.ts`; 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 { computeStoredLayouts, structureAreaRows } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts")); +const { bodyGuardBase } = loadTs(B06("logic/B06_Section_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 { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment.ts")); +const { computeRevetmentLayout } = loadTs(B06("logic/B06_Section_Cross_Revetment_Geom.ts")); const fixture = JSON.parse( fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), diff --git a/resources/tester/helper_80_ford_pipe.cjs b/resources/tester/helper_80_ford_pipe.cjs index 34ca92bb7..d12ecf104 100644 --- a/resources/tester/helper_80_ford_pipe.cjs +++ b/resources/tester/helper_80_ford_pipe.cjs @@ -29,7 +29,7 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", f); -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts")); const fixture = JSON.parse( fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), diff --git a/resources/tester/helper_91_ground_free.cjs b/resources/tester/helper_91_ground_free.cjs index ae12a5ccf..854dc0444 100644 --- a/resources/tester/helper_91_ground_free.cjs +++ b/resources/tester/helper_91_ground_free.cjs @@ -29,7 +29,7 @@ const resolve = (base) => ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", f); -const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts")); +const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts")); const fixture = JSON.parse( fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), diff --git a/resources/tester/test_131_b08_material_fix.py b/resources/tester/test_131_b08_material_fix.py index dd109183e..1ebbf4871 100644 --- a/resources/tester/test_131_b08_material_fix.py +++ b/resources/tester/test_131_b08_material_fix.py @@ -167,7 +167,9 @@ def test_B06_새_키_한_벌(): assert CrossSectionPatch(chainage_m=1, ford_pipe_length_m=9).ford_pipe_length_m == 9 with pytest.raises(ValueError): CrossSectionPatch(chainage_m=1, ford_pipe_length_m=-1) - layouts = (ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts").read_text("utf-8") + layouts = (ROOT / "B06_Section" / "logic" / "B06_Section_Structure_Layouts.ts").read_text( + "utf-8" + ) block = layouts[layouts.index("STRUCTURE_ROW_KEYS = [") :][:900] assert '"ford_pipe_length_m"' in block diff --git a/resources/tester/test_178_1_slope_wall_guard.py b/resources/tester/test_178_1_slope_wall_guard.py index d16520092..142950c7d 100644 --- a/resources/tester/test_178_1_slope_wall_guard.py +++ b/resources/tester/test_178_1_slope_wall_guard.py @@ -76,7 +76,7 @@ def test_관_벽이_서는_측점은_독립_벽을_안_그린다(bundle, tmp_pat def test_카드_서버_3D_가_가드_한_벌을_쓴다(): files = { "카드": ROOT / "B06_Section" / "B06_Section_UI_Cross_View.ts", - "서버 면적 · B07": ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts", + "서버 면적 · B07": ROOT / "B06_Section" / "logic" / "B06_Section_Structure_Layouts.ts", "3D": ROOT / "B05_Profile" / "B05_Profile_UI_Corridor_Structures.ts", } for where, path in files.items(): diff --git a/resources/tester/test_346_3_route_save_screen.py b/resources/tester/test_346_3_route_save_screen.py index 7c6957578..f99857c69 100644 --- a/resources/tester/test_346_3_route_save_screen.py +++ b/resources/tester/test_346_3_route_save_screen.py @@ -134,7 +134,7 @@ def test_넘김_알림은_결과_한_줄에_합침(): keys = _read("B06_Section/B06_Section_Station_Keys.ts") assert "{ quiet = false, applyTo = toRouteId }: { quiet?: boolean; applyTo?: number }" in keys assert "if (outside.size && !quiet)" in keys - patch = _read("B06_Section/B06_Section_Route_Carry_Patch.ts") + patch = _read("B06_Section/logic/B06_Section_Route_Carry_Patch.ts") assert "outside: keyed.outside.size + chain.outside.size" in patch # 498-3 — 결과 한 줄 걷음 · 남김 = 따라감 실패 · 새 교차 관 save = _read("A00_Common/design/design_save.ts") diff --git a/resources/tester/test_445_save_patch.py b/resources/tester/test_445_save_patch.py index 9afe1f1e8..34274eddb 100644 --- a/resources/tester/test_445_save_patch.py +++ b/resources/tester/test_445_save_patch.py @@ -233,7 +233,9 @@ def test_M5_새_행은_버린_수에_안_셈(): def test_H5_노선_저장_뒤_본문_M7_kindreset_옮김(): # 472-6 6-B — 노선 저장 뒤 본문 = 서버 Node 넘김(`routeCarryPatches` · 옛 `saveCachedCrossPatches` · # `sessionOverlay` 몸통 그대로) · 표준 횡단면 · 풍선 = 브라우저 횡단 조각(`routeCarryPart`) - patch = (ROOT / "B06_Section" / "B06_Section_Route_Carry_Patch.ts").read_text(encoding="utf-8") + patch = (ROOT / "B06_Section" / "logic" / "B06_Section_Route_Carry_Patch.ts").read_text( + encoding="utf-8" + ) body = patch[patch.index("export function routeCarryPatches") :] for needle in ( "crossPatchesFromCache(detail, kindChanged, savedAt)", @@ -245,7 +247,10 @@ def test_H5_노선_저장_뒤_본문_M7_kindreset_옮김(): assert needle in body, needle apply = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8") part = apply[apply.index("export function routeCarryPart") :] - for needle in ("readStandardCrossSession(projectId)", "balloon_offsets: balloonOffsetsPayload()"): + for needle in ( + "readStandardCrossSession(projectId)", + "balloon_offsets: balloonOffsetsPayload()", + ): assert needle in part, needle persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8") assert ( diff --git a/resources/tester/test_472_3_calc_registry.py b/resources/tester/test_472_3_calc_registry.py index 4812d13dd..efca83e3c 100644 --- a/resources/tester/test_472_3_calc_registry.py +++ b/resources/tester/test_472_3_calc_registry.py @@ -95,8 +95,6 @@ GEOMETRY_PULLS = ( "B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts", "B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts", "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", @@ -105,16 +103,13 @@ GEOMETRY_PULLS = ( "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", + # 518-3 기하는 logic 으로 · 그리기 함수(띠 · 터파기 · 포장층)를 위 Culvert · Box · Ford 그림 파일이 부름 — 518-2 뒤 걷음 + "B06_Section/B06_Section_UI_Cross_Design_Surface.ts", + "B06_Section/B06_Section_UI_Cross_Excavation.ts", "B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts", - # 노선 바뀐 [저장] 넘김 patch(472-6 6-B) — 세션 칸 → 설계 칸 얹기가 관 벽 · 단 · 세월 · BOX 기본값(위 그림 파일)을 씀 - "B06_Section/B06_Section_Route_Carry_Patch.ts", ) # 묶음 단계 입구(511-B B-1) planline 저장 기준(`plantedBase`) 끌림 11 = B-3 이 logic 으로 옮겨 걷음 KNOWN_PULLS = set(SESSION_PULLS) | set(GEOMETRY_PULLS) diff --git a/resources/tester/test_511_a_calc_mirror.py b/resources/tester/test_511_a_calc_mirror.py index 70177c631..dcf03c8bf 100644 --- a/resources/tester/test_511_a_calc_mirror.py +++ b/resources/tester/test_511_a_calc_mirror.py @@ -75,7 +75,7 @@ def test_quantity_F7_C4_E1_서버_브라우저_같음(tmp_path): conv = _fixture("context.json")["earthwork_conversion"] long_ = detail["longitudinal"] calls = [ - {"mod": "B06_Section/B06_Section_Structure_Layouts.ts", "fn": "structureAreaRows", + {"mod": "B06_Section/logic/B06_Section_Structure_Layouts.ts", "fn": "structureAreaRows", "args": [sections]}, {"mod": "common_util/common_util_mass_haul.ts", "fn": "computeMassHaulSeries", "args": [long_, sections, conv, None, ["cross"]]}, diff --git a/resources/tester/test_511_c_haul_node.py b/resources/tester/test_511_c_haul_node.py index 361782939..9349d8baa 100644 --- a/resources/tester/test_511_c_haul_node.py +++ b/resources/tester/test_511_c_haul_node.py @@ -185,7 +185,7 @@ def test_저장_정본_새_길_브라우저_같음(name, monkeypatch, tmp_path): detail = json.loads((GOLDEN / "detail.json").read_text(encoding="utf-8")) node_input = {"detail": detail, "context": _contexts()[name]} new = prebuild._node_output(copy.deepcopy(node_input)) - layouts = "B06_Section/B06_Section_Structure_Layouts.ts" + layouts = "B06_Section/logic/B06_Section_Structure_Layouts.ts" sections = detail["cross_sections"] areas, walls = _browser( tmp_path, diff --git a/resources/tester/test_b06_pipe_length.py b/resources/tester/test_b06_pipe_length.py index 009458f02..f2cb5ff90 100644 --- a/resources/tester/test_b06_pipe_length.py +++ b/resources/tester/test_b06_pipe_length.py @@ -20,7 +20,7 @@ if str(PROJECT_ROOT) not in sys.path: from B06_Section.B06_Section_Schema import CrossSectionPatch # noqa: E402 -_LAYOUTS = PROJECT_ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts" +_LAYOUTS = PROJECT_ROOT / "B06_Section" / "logic" / "B06_Section_Structure_Layouts.ts" _PERSIST = PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts" _TYPES = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Types.ts" diff --git a/resources/tester/test_b06_wall_drain_pipe.py b/resources/tester/test_b06_wall_drain_pipe.py index f55524466..c26459f0f 100644 --- a/resources/tester/test_b06_wall_drain_pipe.py +++ b/resources/tester/test_b06_wall_drain_pipe.py @@ -29,7 +29,7 @@ const make = (tag) => ({ set textContent(v) { this.text = v; }, }); globalThis.document = { createElementNS: (_ns, tag) => make(tag) }; -const { buildRevetWallGeometry } = await import("./B06_Section_UI_Cross_Wall.js"); +const { buildRevetWallGeometry } = await import("./logic/B06_Section_Cross_Wall_Geom.js"); const { appendWallHatch } = await import("./B06_Section_UI_Cross_Wall_Hatch.js"); const walk = (node, out = []) => { if (node.tag === "g" && node.attrs.class === "b06-chart__wall-drain") out.push(node); @@ -80,6 +80,7 @@ def test_옹벽만_높이_15m_띠마다_파이프_하나(tmp_path: Path) -> None "node", str(TSC), str(B06 / "B06_Section_UI_Cross_Wall.ts"), + str(B06 / "logic" / "B06_Section_Cross_Wall_Geom.ts"), "--outDir", str(out), "--module", @@ -121,7 +122,7 @@ def test_옹벽만_높이_15m_띠마다_파이프_하나(tmp_path: Path) -> None def test_옹벽_표식은_구조물_종류로() -> None: - source = (B06 / "B06_Section_UI_Cross_Revetment.ts").read_text(encoding="utf-8") + source = (B06 / "logic" / "B06_Section_Cross_Revetment_Geom.ts").read_text(encoding="utf-8") # 물빼기 표식 = 구조물 표 형상칸.weep(322-1) — 처음값은 옹벽만 assert "catalogRow(spec.type_id)?.shapeOpts.weep" in source start = json.loads( diff --git a/resources/tester/test_b06_wall_lean_table.py b/resources/tester/test_b06_wall_lean_table.py index 64ebd2036..f9442e311 100644 --- a/resources/tester/test_b06_wall_lean_table.py +++ b/resources/tester/test_b06_wall_lean_table.py @@ -20,7 +20,7 @@ DATA_DIR = ROOT / "resources" / "data_masonry" GEOMETRY = [ "B06_Section_UI_Cross_Culvert_Geom.ts", - "B06_Section_UI_Cross_Wall.ts", + "logic/B06_Section_Cross_Wall_Geom.ts", "B06_Section_UI_Cross_Culvert_Extra.ts", "logic/B06_Section_UI_Cross_Culvert_Solve.ts", ] diff --git a/resources/tester/test_b06_wall_shape_unified.py b/resources/tester/test_b06_wall_shape_unified.py index e79c94898..a968d5de9 100644 --- a/resources/tester/test_b06_wall_shape_unified.py +++ b/resources/tester/test_b06_wall_shape_unified.py @@ -21,7 +21,7 @@ TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" _RUNNER = """ import { writeFileSync } from "node:fs"; -import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall.js"; +import { buildRevetWallGeometry } from "./logic/B06_Section_Cross_Wall_Geom.js"; import { revetWallSpec, REVET_FORMS } from "./logic/B06_Section_UI_Cross_Culvert_Const.js"; const anchor = { offset: 5, elevation: 100 }; const outline = (form, role, extra = {}) => @@ -54,6 +54,7 @@ def test_벽_외곽_한_벌_높이_입력_그대로_노면_멈춤(tmp_path: Path "node", str(TSC), str(B06 / "B06_Section_UI_Cross_Wall.ts"), + str(B06 / "logic" / "B06_Section_Cross_Wall_Geom.ts"), "--outDir", str(out), "--module", @@ -95,8 +96,12 @@ 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 ( + "B06_Section_UI_Cross_Culvert_Geom", + "B06_Section_UI_Cross_Culvert_Extra", + "logic/B06_Section_Cross_Revetment_Geom", + ): + 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")