fix(b06): 묻힌 관 유출 흙막이 벽 모서리 절토 · 3도 끝 연결선 겹침 제거 — 설계선 접힘 해소(186-1)

- 검증_랩탑메인 No.14+12.22 관 유출 흙막이가 원지반 아래 → 91-4 벽 모서리 절토와 139-4 끝 연결선이 함께 이어져 설계선이 되접힘
- 유출 끝 연결선이 있으면 벽 모서리 절토를 잇지 않음(카드 · 서버 면적 · B07 · 3D 같은 기하)
- 사본 재현 시험 test_186_1 · 픽스처 b06_186_1_pipe_292 · 91 헬퍼 시작점 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
2026-10-10 13:10:10 +09:00
co-authored by Claude Opus 5.5
parent 9fa8b30a00
commit 6a582e7f9b
5 changed files with 1113 additions and 3 deletions
@@ -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;
@@ -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"
}
}
]
}
@@ -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));
+3 -2
View File
@@ -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();
@@ -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"]