diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index 5bbd5364a..70e2cd2de 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -601,8 +601,11 @@ export function computeCulvertLayout( // 접점을 다시 깨뜨리지 않는다 — 종전 어긋남의 직접 원인이 이 순서였다. const slopeOf = (wall: WallLayout): FillSlopeSegment => fillSlopeOf(wall, wall.role === "inlet" ? inletInfo.edge : outletInfo.edge); + // 유출 끝 연결선(139-4)이 있으면 벽 모서리 절토(91-4)를 또 잇지 않음 — 설계선이 접힘(186-1) + const outletTail = drawnFillPoints(extrasForArea, true).length > 0; for (const wall of walls) { - const slope = withBackCut(slopeOf(wall), wall, section.design, groundAt, !culvert.hidden_pipe); + const backCut = !culvert.hidden_pipe && !outletTail; + const slope = withBackCut(slopeOf(wall), wall, section.design, groundAt, backCut); // 사면선은 노견부터 우리가 그림 — 트림을 노견까지 당겨 백엔드 설계선 성토 구간을 통째로 덮음 if (wall.outward > 0) { trimMax = slope.points[0].offset; diff --git a/resources/tester/fixtures/b06_186_1_pipe_292.json b/resources/tester/fixtures/b06_186_1_pipe_292.json new file mode 100644 index 000000000..6459b8908 --- /dev/null +++ b/resources/tester/fixtures/b06_186_1_pipe_292.json @@ -0,0 +1,917 @@ +{ + "source": "검증_랩탑메인(226fc805) route 282 · No.14+12.22 관(파형강관 D800 · 유입 L형 집수정 · 유출 흙막이 돌쌓기(찰) H1.3) · 저장 design 그대로(유출 d 9.0)", + "cross_sections": [ + { + "station_id": "station_000000292220", + "chainage_m": 292.22, + "label": "STA.0+292.220", + "kind": "irregular", + "center_x": 208535.186869, + "center_y": 367765.719131, + "center_z": 860.254154, + "azimuth_deg": 134.945611, + "plan_radius_m": null, + "curve_outer_side": "left", + "curve_widening_m": 2.033, + "uphill_side": "right", + "structure": "파형강관 D800", + "samples": [ + { + "offset_m": -20.0, + "elevation_m": 875.942681, + "valid": true + }, + { + "offset_m": -19.5, + "elevation_m": 875.563445, + "valid": true + }, + { + "offset_m": -19.0, + "elevation_m": 875.223804, + "valid": true + }, + { + "offset_m": -18.5, + "elevation_m": 874.839366, + "valid": true + }, + { + "offset_m": -18.0, + "elevation_m": 874.373517, + "valid": true + }, + { + "offset_m": -17.5, + "elevation_m": 873.834699, + "valid": true + }, + { + "offset_m": -17.0, + "elevation_m": 873.328076, + "valid": true + }, + { + "offset_m": -16.5, + "elevation_m": 873.003181, + "valid": true + }, + { + "offset_m": -16.0, + "elevation_m": 872.868685, + "valid": true + }, + { + "offset_m": -15.5, + "elevation_m": 872.700062, + "valid": true + }, + { + "offset_m": -15.0, + "elevation_m": 872.492793, + "valid": true + }, + { + "offset_m": -14.5, + "elevation_m": 872.264882, + "valid": true + }, + { + "offset_m": -14.0, + "elevation_m": 871.921377, + "valid": true + }, + { + "offset_m": -13.5, + "elevation_m": 871.449263, + "valid": true + }, + { + "offset_m": -13.0, + "elevation_m": 871.050131, + "valid": true + }, + { + "offset_m": -12.5, + "elevation_m": 870.664549, + "valid": true + }, + { + "offset_m": -12.0, + "elevation_m": 870.348929, + "valid": true + }, + { + "offset_m": -11.5, + "elevation_m": 869.897268, + "valid": true + }, + { + "offset_m": -11.0, + "elevation_m": 869.481532, + "valid": true + }, + { + "offset_m": -10.5, + "elevation_m": 869.031119, + "valid": true + }, + { + "offset_m": -10.0, + "elevation_m": 868.558707, + "valid": true + }, + { + "offset_m": -9.5, + "elevation_m": 868.123739, + "valid": true + }, + { + "offset_m": -9.0, + "elevation_m": 867.798147, + "valid": true + }, + { + "offset_m": -8.5, + "elevation_m": 867.489499, + "valid": true + }, + { + "offset_m": -8.0, + "elevation_m": 867.256843, + "valid": true + }, + { + "offset_m": -7.5, + "elevation_m": 867.081439, + "valid": true + }, + { + "offset_m": -7.0, + "elevation_m": 866.940587, + "valid": true + }, + { + "offset_m": -6.5, + "elevation_m": 866.550692, + "valid": true + }, + { + "offset_m": -6.0, + "elevation_m": 865.812117, + "valid": true + }, + { + "offset_m": -5.5, + "elevation_m": 864.812694, + "valid": true + }, + { + "offset_m": -5.0, + "elevation_m": 864.211627, + "valid": true + }, + { + "offset_m": -4.5, + "elevation_m": 863.993389, + "valid": true + }, + { + "offset_m": -4.0, + "elevation_m": 863.791331, + "valid": true + }, + { + "offset_m": -3.5, + "elevation_m": 863.6089, + "valid": true + }, + { + "offset_m": -3.0, + "elevation_m": 863.47762, + "valid": true + }, + { + "offset_m": -2.5, + "elevation_m": 863.178131, + "valid": true + }, + { + "offset_m": -2.0, + "elevation_m": 862.70616, + "valid": true + }, + { + "offset_m": -1.5, + "elevation_m": 862.084857, + "valid": true + }, + { + "offset_m": -1.0, + "elevation_m": 861.397999, + "valid": true + }, + { + "offset_m": -0.5, + "elevation_m": 860.781491, + "valid": true + }, + { + "offset_m": 0.0, + "elevation_m": 860.254154, + "valid": true + }, + { + "offset_m": 0.5, + "elevation_m": 860.008424, + "valid": true + }, + { + "offset_m": 1.0, + "elevation_m": 859.743024, + "valid": true + }, + { + "offset_m": 1.5, + "elevation_m": 859.431951, + "valid": true + }, + { + "offset_m": 2.0, + "elevation_m": 859.169405, + "valid": true + }, + { + "offset_m": 2.5, + "elevation_m": 858.865535, + "valid": true + }, + { + "offset_m": 3.0, + "elevation_m": 858.524512, + "valid": true + }, + { + "offset_m": 3.5, + "elevation_m": 858.164126, + "valid": true + }, + { + "offset_m": 4.0, + "elevation_m": 857.818061, + "valid": true + }, + { + "offset_m": 4.5, + "elevation_m": 857.442262, + "valid": true + }, + { + "offset_m": 5.0, + "elevation_m": 857.021062, + "valid": true + }, + { + "offset_m": 5.5, + "elevation_m": 856.664537, + "valid": true + }, + { + "offset_m": 6.0, + "elevation_m": 856.388529, + "valid": true + }, + { + "offset_m": 6.5, + "elevation_m": 856.164636, + "valid": true + }, + { + "offset_m": 7.0, + "elevation_m": 855.840671, + "valid": true + }, + { + "offset_m": 7.5, + "elevation_m": 855.415507, + "valid": true + }, + { + "offset_m": 8.0, + "elevation_m": 855.15202, + "valid": true + }, + { + "offset_m": 8.5, + "elevation_m": 854.88336, + "valid": true + }, + { + "offset_m": 9.0, + "elevation_m": 854.52968, + "valid": true + }, + { + "offset_m": 9.5, + "elevation_m": 854.210706, + "valid": true + }, + { + "offset_m": 10.0, + "elevation_m": 853.936088, + "valid": true + }, + { + "offset_m": 10.5, + "elevation_m": 853.667609, + "valid": true + }, + { + "offset_m": 11.0, + "elevation_m": 853.315257, + "valid": true + }, + { + "offset_m": 11.5, + "elevation_m": 852.962466, + "valid": true + }, + { + "offset_m": 12.0, + "elevation_m": 852.592563, + "valid": true + }, + { + "offset_m": 12.5, + "elevation_m": 852.365461, + "valid": true + }, + { + "offset_m": 13.0, + "elevation_m": 852.241766, + "valid": true + }, + { + "offset_m": 13.5, + "elevation_m": 852.168603, + "valid": true + }, + { + "offset_m": 14.0, + "elevation_m": 852.137894, + "valid": true + }, + { + "offset_m": 14.5, + "elevation_m": 852.122192, + "valid": true + }, + { + "offset_m": 15.0, + "elevation_m": 852.082728, + "valid": true + }, + { + "offset_m": 15.5, + "elevation_m": 851.581695, + "valid": true + }, + { + "offset_m": 16.0, + "elevation_m": 850.702327, + "valid": true + }, + { + "offset_m": 16.5, + "elevation_m": 849.751282, + "valid": true + }, + { + "offset_m": 17.0, + "elevation_m": 849.271151, + "valid": true + }, + { + "offset_m": 17.5, + "elevation_m": 849.023418, + "valid": true + }, + { + "offset_m": 18.0, + "elevation_m": 848.775996, + "valid": true + }, + { + "offset_m": 18.5, + "elevation_m": 848.584444, + "valid": true + }, + { + "offset_m": 19.0, + "elevation_m": 848.552501, + "valid": true + }, + { + "offset_m": 19.5, + "elevation_m": 848.524788, + "valid": true + }, + { + "offset_m": 20.0, + "elevation_m": 848.338541, + "valid": true + } + ], + "culvert": { + "type": "pipe", + "pipe_kind": "파형강관", + "diameter_m": 0.8, + "min_cover_m": 0.5, + "foundation": null, + "inlet": { + "role": "inlet", + "structure": "집수정", + "basin_length_m": 2.0, + "basin_before_m": 1.0, + "basin_after_m": 1.0, + "basin_height_m": 1.3, + "basin_form": "L" + }, + "outlet": { + "role": "outlet", + "structure": "기슭막이", + "revet_form": "돌쌓기(찰)", + "revet_height_m": 1.3, + "revet_length_m": 10.0, + "revet_before_m": 5.0, + "revet_after_m": 5.0, + "face_slope": 0.3 + }, + "chainage_m": 292.22 + }, + "design": { + "ground_type": "ripping_rock", + "geometry_preset": "rock", + "section_mode": "both_cut", + "ditch_side": "right", + "ditch_type": "standard", + "cut_slope_ratio": 0.4, + "soil_cut_slope_ratio": 1.0, + "two_stage_slope": true, + "fill_slope_ratio": 1.2, + "roadbed_width_m": 6.0, + "carriageway_width_m": 5.0, + "carriageway_standard_width_m": 3.0, + "widening_left_m": 2.0, + "widening_right_m": 0.0, + "cross_slope_pct": 3.0, + "ditch": { + "type": "standard", + "top_width_m": 0.9, + "bottom_width_m": 0.3, + "depth_m": 0.3 + }, + "ditch_enabled": true, + "ditch_choice": null, + "paved": false, + "road_edges": { + "left": { + "offset_m": 4.0, + "elevation_m": 857.4906 + }, + "right": { + "offset_m": -2.0, + "elevation_m": 857.3106 + } + }, + "carriageway_edges": { + "left": { + "offset_m": 3.5, + "elevation_m": 857.4756 + }, + "right": { + "offset_m": -1.5, + "elevation_m": 857.3256 + } + }, + "design_elevation_m": 857.3706, + "cut_area_m2": 27.9382, + "cut_soil_area_m2": 8.3598, + "cut_rock_area_m2": 19.5784, + "cut_rock_kind": "ripping_rock", + "fill_area_m2": 0.3364, + "spoil_fill_area_m2": 0.0, + "spoil_fill_side": null, + "spoil_fill_width_m": 0.0, + "spoil_fill_max_width_m": 0.0, + "spoil_fill_line": [], + "spoil_fill_unclosed": false, + "spoil_fill_replaced_fill_m2": 0.0, + "bench_cut_length_m": 0.0, + "slope_unclosed": false, + "fill_ground_slope": null, + "ditch_area_m2": 0.18, + "ditch_soil_area_m2": 0.0, + "ditch_rock_area_m2": 0.18, + "ditch_split_basis": "rock_boundary", + "design_line": [ + { + "offset_m": -20.0, + "elevation_m": 875.9427 + }, + { + "offset_m": -19.5, + "elevation_m": 875.5634 + }, + { + "offset_m": -19.0, + "elevation_m": 875.2238 + }, + { + "offset_m": -18.5, + "elevation_m": 874.8394 + }, + { + "offset_m": -18.0, + "elevation_m": 874.3735 + }, + { + "offset_m": -17.5, + "elevation_m": 873.8347 + }, + { + "offset_m": -17.0, + "elevation_m": 873.3281 + }, + { + "offset_m": -16.5, + "elevation_m": 873.0032 + }, + { + "offset_m": -16.0, + "elevation_m": 872.8687 + }, + { + "offset_m": -15.5, + "elevation_m": 872.7001 + }, + { + "offset_m": -15.0, + "elevation_m": 872.4928 + }, + { + "offset_m": -14.5, + "elevation_m": 872.2649 + }, + { + "offset_m": -14.0, + "elevation_m": 871.9214 + }, + { + "offset_m": -13.5, + "elevation_m": 871.4493 + }, + { + "offset_m": -13.0, + "elevation_m": 871.0501 + }, + { + "offset_m": -12.5, + "elevation_m": 870.6645 + }, + { + "offset_m": -12.0, + "elevation_m": 870.3489 + }, + { + "offset_m": -11.5, + "elevation_m": 869.8973 + }, + { + "offset_m": -11.0, + "elevation_m": 869.4815 + }, + { + "offset_m": -10.5, + "elevation_m": 869.0311 + }, + { + "offset_m": -10.0, + "elevation_m": 868.5587 + }, + { + "offset_m": -9.5, + "elevation_m": 868.1237 + }, + { + "offset_m": -9.0, + "elevation_m": 867.7981 + }, + { + "offset_m": -8.5, + "elevation_m": 867.4895 + }, + { + "offset_m": -8.0, + "elevation_m": 867.2568 + }, + { + "offset_m": -7.75, + "elevation_m": 867.1691 + }, + { + "offset_m": -7.5, + "elevation_m": 866.9294 + }, + { + "offset_m": -7.0, + "elevation_m": 866.4294 + }, + { + "offset_m": -6.5, + "elevation_m": 865.9294 + }, + { + "offset_m": -6.2459, + "elevation_m": 865.6753 + }, + { + "offset_m": -6.0, + "elevation_m": 865.0606 + }, + { + "offset_m": -5.5, + "elevation_m": 863.8106 + }, + { + "offset_m": -5.0, + "elevation_m": 862.5606 + }, + { + "offset_m": -4.5, + "elevation_m": 861.3106 + }, + { + "offset_m": -4.0, + "elevation_m": 860.0606 + }, + { + "offset_m": -3.5, + "elevation_m": 858.8106 + }, + { + "offset_m": -3.0, + "elevation_m": 857.5606 + }, + { + "offset_m": -2.9, + "elevation_m": 857.3106 + }, + { + "offset_m": -2.6, + "elevation_m": 857.0106 + }, + { + "offset_m": -2.5, + "elevation_m": 857.0106 + }, + { + "offset_m": -2.3, + "elevation_m": 857.0106 + }, + { + "offset_m": -2.0, + "elevation_m": 857.3106 + }, + { + "offset_m": -1.5, + "elevation_m": 857.3256 + }, + { + "offset_m": -1.0, + "elevation_m": 857.3406 + }, + { + "offset_m": -0.5, + "elevation_m": 857.3556 + }, + { + "offset_m": 0.0, + "elevation_m": 857.3706 + }, + { + "offset_m": 0.5, + "elevation_m": 857.3856 + }, + { + "offset_m": 1.0, + "elevation_m": 857.4006 + }, + { + "offset_m": 1.5, + "elevation_m": 857.4156 + }, + { + "offset_m": 2.0, + "elevation_m": 857.4306 + }, + { + "offset_m": 2.5, + "elevation_m": 857.4456 + }, + { + "offset_m": 3.0, + "elevation_m": 857.4606 + }, + { + "offset_m": 3.5, + "elevation_m": 857.4756 + }, + { + "offset_m": 4.0, + "elevation_m": 857.4906 + }, + { + "offset_m": 4.2, + "elevation_m": 857.6677 + }, + { + "offset_m": 4.5, + "elevation_m": 857.4423 + }, + { + "offset_m": 5.0, + "elevation_m": 857.0211 + }, + { + "offset_m": 5.5, + "elevation_m": 856.6645 + }, + { + "offset_m": 6.0, + "elevation_m": 856.3885 + }, + { + "offset_m": 6.5, + "elevation_m": 856.1646 + }, + { + "offset_m": 7.0, + "elevation_m": 855.8407 + }, + { + "offset_m": 7.5, + "elevation_m": 855.4155 + }, + { + "offset_m": 8.0, + "elevation_m": 855.152 + }, + { + "offset_m": 8.5, + "elevation_m": 854.8834 + }, + { + "offset_m": 9.0, + "elevation_m": 854.5297 + }, + { + "offset_m": 9.5, + "elevation_m": 854.2107 + }, + { + "offset_m": 10.0, + "elevation_m": 853.9361 + }, + { + "offset_m": 10.5, + "elevation_m": 853.6676 + }, + { + "offset_m": 11.0, + "elevation_m": 853.3153 + }, + { + "offset_m": 11.5, + "elevation_m": 852.9625 + }, + { + "offset_m": 12.0, + "elevation_m": 852.5926 + }, + { + "offset_m": 12.5, + "elevation_m": 852.3655 + }, + { + "offset_m": 13.0, + "elevation_m": 852.2418 + }, + { + "offset_m": 13.5, + "elevation_m": 852.1686 + }, + { + "offset_m": 14.0, + "elevation_m": 852.1379 + }, + { + "offset_m": 14.5, + "elevation_m": 852.1222 + }, + { + "offset_m": 15.0, + "elevation_m": 852.0827 + }, + { + "offset_m": 15.5, + "elevation_m": 851.5817 + }, + { + "offset_m": 16.0, + "elevation_m": 850.7023 + }, + { + "offset_m": 16.5, + "elevation_m": 849.7513 + }, + { + "offset_m": 17.0, + "elevation_m": 849.2712 + }, + { + "offset_m": 17.5, + "elevation_m": 849.0234 + }, + { + "offset_m": 18.0, + "elevation_m": 848.776 + }, + { + "offset_m": 18.5, + "elevation_m": 848.5844 + }, + { + "offset_m": 19.0, + "elevation_m": 848.5525 + }, + { + "offset_m": 19.5, + "elevation_m": 848.5248 + }, + { + "offset_m": 20.0, + "elevation_m": 848.3385 + } + ], + "cut_slope_segments": [ + { + "side": "left", + "ratio": 1.0, + "rise_m": 0.2, + "run_m": 0.2, + "start_offset_m": 4.0, + "end_offset_m": 4.2, + "material": "soil" + }, + { + "side": "right", + "ratio": 0.4, + "rise_m": 8.3647, + "run_m": 3.3459, + "start_offset_m": -2.9, + "end_offset_m": -6.2459, + "material": "rock" + }, + { + "side": "right", + "ratio": 1.0, + "rise_m": 1.5041, + "run_m": 1.5041, + "start_offset_m": -6.2459, + "end_offset_m": -7.75, + "material": "soil" + } + ], + "rock_boundary_offset_m": -0.5, + "status": "provisional", + "pavement_suggested": false, + "revet_adjust": { + "outlet": { + "x": 0.0, + "d": 9.0, + "h": null, + "m": null + } + }, + "pipe_length_m": 18.0, + "pipe_slope_pct": 42.0261, + "extra_walls": [], + "wall_fill_overlap": { + "outlet": 0 + }, + "inlet_basin": "L" + } + } + ] +} \ No newline at end of file diff --git a/resources/tester/helper_186_1_outlet_fold.cjs b/resources/tester/helper_186_1_outlet_fold.cjs new file mode 100644 index 000000000..14d84df96 --- /dev/null +++ b/resources/tester/helper_186_1_outlet_fold.cjs @@ -0,0 +1,116 @@ +/* 186-1 묻힌 관 유출 벽 설계선 접힘 헬퍼 — 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림. + * 입력 = 검증_랩탑메인 No.14+12.22 사본(`fixtures/b06_186_1_pipe_292.json`) · 73-4 픽스처 관 340(절토 쪽 유출). + * 결과 = 표준출력 JSON 한 벌. 짝 = `test_186_1_outlet_fold.py`. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +// 화면 전용 모듈(토스트) — 기하 계산엔 안 쓰임 +const STUBS = { + "@ui/ui_template_toast": { showToast: () => undefined }, +}; + +function loadTs(file) { + const full = path.resolve(file); + if (cache.has(full)) return cache.get(full); + if (full.endsWith(".json")) { + const data = JSON.parse(fs.readFileSync(full, "utf8")); + cache.set(full, data); + return data; + } + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + esModuleInterop: true, + }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => { + if (STUBS[request]) return STUBS[request]; + const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; + for (const [prefix, dir] of Object.entries(alias)) + if (request.startsWith(prefix)) + return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length)))); + return loadTs(resolve(path.join(path.dirname(full), request))); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + cache.set(full, box.exports); + return box.exports; +} +const resolve = (base) => + [base, `${base}.ts`, path.join(base, "index.ts")].find( + (p) => fs.existsSync(p) && fs.statSync(p).isFile(), + ) ?? `${base}.ts`; + +const B06 = (f) => path.join(ROOT, "B06_Section", f); +const { computeStoredLayouts, structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts.ts")); +const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts")); +const read = (name) => + JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", name), "utf8")).cross_sections; +const clone = (v) => JSON.parse(JSON.stringify(v)); +const r2 = (v) => Math.round(v * 100) / 100; + +/** 유출 쪽 설계선(트림 사면) 점열 · 유출 벽 · 면적 행. */ +function study(sections, target) { + const section = sections.find((s) => Math.abs(s.chainage_m - target) < 0.01); + const { culvert } = computeStoredLayouts(section, sections); + const wall = culvert.walls.find((w) => w.role === "outlet"); + const trim = culvert.designTrim; + const slope = (wall.outward > 0 ? trim.maxSlope : trim.minSlope).points; + const row = structureAreaRows(sections).find((r) => Math.abs(r.chainage_m - target) < 0.01); + return { + outward: wall.outward, + wallTop: r2(wall.topJoint.elevation), + groundAtWall: r2(groundOf(section, wall.topJoint.offset)), + offsets: slope.map((p) => r2(p.offset)), + elevations: slope.map((p) => r2(p.elevation)), + cut: row ? row.cut_area_m2 : null, + fill: row ? row.fill_area_m2 : null, + }; +} +function groundOf(section, offset) { + const s = section.samples; + for (let i = 1; i < s.length; i += 1) + if (s[i].offset_m >= offset) { + const t = (offset - s[i - 1].offset_m) / (s[i].offset_m - s[i - 1].offset_m); + return s[i - 1].elevation_m + (s[i].elevation_m - s[i - 1].elevation_m) * t; + } + return NaN; +} + +const out = {}; +// ① 저장값 그대로(유출 d 9.0) · ② 자동 자리(조작값 없음) +const stored = read("b06_186_1_pipe_292.json"); +out.stored = study(stored, 292.22); +const auto = clone(stored); +auto[0].design.revet_adjust = null; +out.auto = study(auto, 292.22); +// ③ 91-4 절토 쪽 배관 유출(73-4 픽스처 관 340 · 유출 = 우측 절토) +const base = read("b06_73_4_body_guard_sections.json"); +const s340 = clone(base.find((s) => Math.abs(s.chainage_m - 340) < 0.6)); +s340.uphill_side = "left"; +s340.culvert = { + type: "pipe", + chainage_m: 340, + pipe_kind: "흄관", + diameter_m: 1.0, + min_cover_m: 0.5, + inlet: { role: "inlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" }, + outlet: { role: "outlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" }, +}; +s340.design.inlet_structure = "revet"; +const layout = computeCulvertLayout(s340, s340.samples, undefined, "revet"); +const wall340 = layout.walls.find((w) => w.role === "outlet"); +const slope340 = (wall340.outward > 0 ? layout.designTrim.maxSlope : layout.designTrim.minSlope) + .points; +out.cut340 = { + outward: wall340.outward, + offsets: slope340.map((p) => r2(p.offset)), + elevations: slope340.map((p) => r2(p.elevation)), +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_91_ground_free.cjs b/resources/tester/helper_91_ground_free.cjs index 5cb2a0ec8..4b34502db 100644 --- a/resources/tester/helper_91_ground_free.cjs +++ b/resources/tester/helper_91_ground_free.cjs @@ -185,9 +185,10 @@ function cutOutlet(edit = () => undefined) { s.design.inlet_structure = "revet"; edit(s); const layout = computeCulvertLayout(s, s.samples, undefined, "revet"); - const wall = layout.walls.find((w) => w.role === "outlet"); + // 벽 뒤 절토 = 유출 끝 연결선(139-4) — 벽 모서리 절토는 또 잇지 않음(186-1) + const start = layout.outletFill.segments.at(-1).points[0]; const pts = layout.designTrim.minSlope.points; - const from = pts.findIndex((p) => Math.abs(p.offset - wall.points[2].offset) < 1e-9); + const from = pts.findIndex((p) => Math.abs(p.offset - start.offset) < 1e-9); return profile(pts.slice(from), s.samples); } out.cutOutlet = cutOutlet(); diff --git a/resources/tester/test_186_1_outlet_fold.py b/resources/tester/test_186_1_outlet_fold.py new file mode 100644 index 000000000..c477551ca --- /dev/null +++ b/resources/tester/test_186_1_outlet_fold.py @@ -0,0 +1,73 @@ +"""PLAN 186-1 — 흙막이류 횡단 형상(랩탑 메인 프로젝트 No.14+12) 사본 재현. + +자료: 검증_랩탑메인(226fc805) No.14+12.22 = 관(파형강관 D800) · 유입 L형 집수정 · 유출 흙막이 +(돌쌓기(찰) H1.3) · 유출 쪽 원지반이 노견보다 높아 벽이 묻힘(벽 윗면 849.99 · 원지반 852.24). + +흠: 묻힌 유출 벽에 절토선이 둘 — 벽 바깥 윗모서리 절토(91-4 `withBackCut`)와 관 끝 3도 절토선 +(139-4). 둘 다 설계선(트림 사면)에 이어 붙어 선이 원지반까지 올랐다가 벽 앞으로 되내려 접힘 +(오프셋 13.45 → 14.59 → 13.69) → 카드 · B07 · 3D 모양이 꺾이고 서버 면적도 접힌 폐회로로 셈. + +고침: 유출 끝 연결선(139-4)이 있으면 벽 모서리 절토를 잇지 않음(`_Culvert_Geom.ts`). +""" + +from __future__ import annotations + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(__file__) +ROOT = os.path.join(HERE, "..", "..") + +pytestmark = pytest.mark.skipif( + not os.path.isdir(os.path.join(ROOT, "config", "node_modules")), + reason="프론트엔드 의존성 미설치", +) + + +@pytest.fixture(scope="module") +def out() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", os.path.join(HERE, "helper_186_1_outlet_fold.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def _one_way(offsets: list[float], outward: int) -> bool: + """설계선이 바깥으로만 나아감 — 되돌아가면 접힌 것.""" + return all((b - a) * outward >= -1e-9 for a, b in zip(offsets, offsets[1:])) + + +def _no_peak(elevations: list[float]) -> bool: + """원지반까지 올랐다 되내리는 뾰족 봉우리 없음.""" + return not any(b > a and b > c for a, b, c in zip(elevations, elevations[1:], elevations[2:])) + + +@pytest.mark.parametrize("case", ["stored", "auto"]) +def test_묻힌_유출_벽_설계선이_안_접힘(out: dict, case: str) -> None: + """저장값(d 9.0)도 자동 자리도 — 벽은 묻혀 있고 설계선은 한 방향.""" + study = out[case] + assert study["wallTop"] < study["groundAtWall"] # 사본이 묻힌 벽을 재현 + assert _one_way(study["offsets"], study["outward"]), study["offsets"] + assert _no_peak(study["elevations"]), study["elevations"] + + +def test_저장값_사본_면적(out: dict) -> None: + """접힌 폐회로(절토 27.94 · 성토 0.34 ㎡) → 노견~벽~3도 선 절토만.""" + assert out["stored"]["cut"] == pytest.approx(34.0391, abs=1e-3) + assert out["stored"]["fill"] == 0 + assert out["auto"]["cut"] == pytest.approx(16.8211, abs=1e-3) + + +def test_절토_쪽_배관_유출도_안_접힘(out: dict) -> None: + """91-4 자료(관 340 · 유출 = 절토 쪽) — 3도 선이 못 만나 복합경사 하나로 원지반까지.""" + study = out["cut340"] + assert _one_way(study["offsets"], study["outward"]), study["offsets"] + assert _no_peak(study["elevations"]), study["elevations"]