Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
* 평면 발자국 — 측점 횡단 설계 결과를 평면 선으로(도로 끝 좌 · 우 · 비탈 끝 깎기 · 쌓기 · 눈금) (346-2)
|
||||
*
|
||||
* ⚠⚠ 짝: `B07_DesignDetail/B07_DesignDetail_Engine_Cad_PlanSlope.py`
|
||||
* (`route_frame` · `route_normal` · `side_is_cut` · `slope_end` · `slope_pieces` 와 그 아래
|
||||
* 단조 3차 보간 · 촘촘히 · 접힘 버림 · 353-1 · 모서리 둥글림 · 364-3) — 한쪽만 고치면
|
||||
* (`route_frame` · `route_normal` · `side_is_cut` · `slope_end` · `final_slope_end` ·
|
||||
* `slope_pieces` 와 그 아래 단조 3차 보간 · 촘촘히 · 접힘 버림 · 353-1 · 모서리 둥글림 · 364-3) — 한쪽만 고치면
|
||||
* 종단 · 횡단 편집 중 평면과 B07 계획평면도가 갈린다.
|
||||
* `footprintRecord` = `B07_DesignDetail_Router_Support_Plan.slope_records` 의 측점 한 줄.
|
||||
* 거울 시험: `resources/tester/test_346_2_plan_footprint_mirror.py`
|
||||
@@ -35,6 +35,9 @@ export interface FootprintStation {
|
||||
right?: FootprintPoint | null;
|
||||
} | null;
|
||||
design_line?: FootprintPoint[] | null;
|
||||
/** 구조물 처리 뒤 비탈 끝(B06 `STRUCTURE_ROW_KEYS` · 벽 측점만 · 578-2). */
|
||||
slope_end_left_m?: number | null;
|
||||
slope_end_right_m?: number | null;
|
||||
} | null;
|
||||
samples: FootprintPoint[];
|
||||
}
|
||||
@@ -156,6 +159,20 @@ export function slopeEnd(
|
||||
return end;
|
||||
}
|
||||
|
||||
/** 구조물 처리 뒤 비탈 끝 — 벽 측점은 B06 이 얹은 `slope_end_<쪽>_m`(578-2) · 없으면 설계선 끝.
|
||||
* 짝: `final_slope_end`. */
|
||||
export function finalSlopeEnd(
|
||||
design: NonNullable<FootprintStation["design"]>,
|
||||
samples: FootprintPoint[],
|
||||
edge: number,
|
||||
side: Side,
|
||||
): number {
|
||||
const stored = design[`slope_end_${side}_m`];
|
||||
return typeof stored === "number"
|
||||
? stored
|
||||
: slopeEnd(design.design_line ?? [], samples, edge, side);
|
||||
}
|
||||
|
||||
/** 측점 한 자리 → `slope_pieces` 줄 · 도로 끝이 없으면 null. 짝: `slope_records` 한 줄. */
|
||||
export function footprintRecord(station: FootprintStation): FootprintRecord | null {
|
||||
const edges = station.design?.road_edges;
|
||||
@@ -163,14 +180,14 @@ export function footprintRecord(station: FootprintStation): FootprintRecord | nu
|
||||
const right = Number(edges?.right?.offset_m);
|
||||
if (edges?.left?.offset_m == null || edges?.right?.offset_m == null) return null;
|
||||
if (!Number.isFinite(left) || !Number.isFinite(right)) return null;
|
||||
const line = station.design?.design_line ?? [];
|
||||
const design = station.design ?? {};
|
||||
const samples = station.samples ?? [];
|
||||
const record: FootprintRecord = {
|
||||
chainage_m: station.chainage_m,
|
||||
left_edge_m: left,
|
||||
left_end_m: slopeEnd(line, samples, left, "left"),
|
||||
left_end_m: finalSlopeEnd(design, samples, left, "left"),
|
||||
right_edge_m: right,
|
||||
right_end_m: slopeEnd(line, samples, right, "right"),
|
||||
right_end_m: finalSlopeEnd(design, samples, right, "right"),
|
||||
};
|
||||
for (const side of SIDES) {
|
||||
const elevation = edges[side]?.elevation_m;
|
||||
|
||||
@@ -73,6 +73,8 @@ _AREA_KEYS = (
|
||||
"box_ground_slope_pct",
|
||||
"box_length_m",
|
||||
"ford_pipe_length_m",
|
||||
"slope_end_left_m",
|
||||
"slope_end_right_m",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
import { computeFordLayout, fordAdjustOf } from "./B06_Section_Cross_Ford_Geom";
|
||||
import type { FordLayout } from "./B06_Section_Cross_Ford_Geom";
|
||||
import { computeRevetmentLayout } from "./B06_Section_Cross_Revetment_Geom";
|
||||
import type { RevetmentLayout } from "./B06_Section_Cross_Revetment_Geom";
|
||||
|
||||
/** 같은 측점으로 볼 누가거리 오차(m) — 정본 반올림 자릿수보다 크게 잡는다. */
|
||||
const CHAINAGE_TOLERANCE_M = 0.02;
|
||||
@@ -163,6 +164,9 @@ export const STRUCTURE_ROW_KEYS = [
|
||||
"box_length_m",
|
||||
// B08 자재 집계표 세월교 관 한 련 길이(131) — 그린 관 길이 1 m 올림 · 같은 길로 실음.
|
||||
"ford_pipe_length_m",
|
||||
// 평면 비탈 끝(578-2) — 벽이 선 쪽 횡단 최종 모양이 땅과 만나는 오프셋 · 평면 발자국 · B07 계획평면도가 읽음.
|
||||
"slope_end_left_m",
|
||||
"slope_end_right_m",
|
||||
] as const;
|
||||
|
||||
/** 박스 두 끝(구체 끝) 원지반 경사(%) — 유입 − 유출 ÷ 수평거리. 본체는 늘 수평이라 원지반으로(110). */
|
||||
@@ -191,6 +195,29 @@ function fordPipeLengthM(ford: FordLayout): number | null {
|
||||
return length > 0 ? Math.ceil(length - 1e-6) : null;
|
||||
}
|
||||
|
||||
/** 벽(옹벽 · 흙막이 · 돌쌓기)이 선 쪽 — 구조물 처리 뒤 횡단이 땅과 만나는 오프셋(578-2).
|
||||
* 윤곽 = 트림 사면(노견 → 벽 이음) → 단마다 벽 윗면 · 앞면(→ 전면 발끝) · 사이 성토부선(3D 실루엣 차례).
|
||||
* 위에서 내려와 처음 땅에 닿는 자리 · 끝까지 안 닿으면(뜬 벽) 마지막 발끝 · 노견부터 땅 밑이면 null. */
|
||||
function wallSlopeEnd(section: CrossSection, own: RevetmentLayout): number | null {
|
||||
const groundAt = groundInterpolator(section.samples);
|
||||
const trim = own.designTrim;
|
||||
const chain = [...((own.side === "left" ? trim.maxSlope : trim.minSlope)?.points ?? [])];
|
||||
const segments = own.fillSegments.filter((segment) => segment.kind !== "cut");
|
||||
own.walls.forEach((wall, index) => {
|
||||
// 벽 도형 = [뒤 밑, 뒤 위, 앞 위, 앞 밑](`buildRevetWallGeometry`) — 앞면이 그려지는 모양 그대로
|
||||
chain.push(wall.fillTop, ...wall.points.slice(2), ...(segments[index]?.points ?? []));
|
||||
});
|
||||
if (!groundAt || chain.length < 2) return null;
|
||||
let [prev, above] = [chain[0], chain[0].elevation - groundAt(chain[0].offset)];
|
||||
if (above <= 0) return null;
|
||||
for (const point of chain.slice(1)) {
|
||||
const now = point.elevation - groundAt(point.offset);
|
||||
if (now <= 0) return prev.offset + ((point.offset - prev.offset) * above) / (above - now);
|
||||
[prev, above] = [point, now];
|
||||
}
|
||||
return prev.offset;
|
||||
}
|
||||
|
||||
/** 폐회로 면적 입력(설계선·지반선·트림) — 트림이나 설계선이 모자라면 null. */
|
||||
function areaInputOf(section: CrossSection, layouts: StoredLayouts) {
|
||||
const trim = trimOfLayouts(layouts);
|
||||
@@ -254,6 +281,11 @@ function areaRowOf(
|
||||
const fordLength =
|
||||
layouts.ford && isOwner(section.ford?.chainage_m) ? fordPipeLengthM(layouts.ford) : null;
|
||||
if (fordLength !== null) ownRow.ford_pipe_length_m = fordLength;
|
||||
// 벽 트림이 그려지는 측점만(관 · 세월교 · BOX 트림이 앞서면 그쪽 모양 · 578-2 범위 밖)
|
||||
const own = layouts.own;
|
||||
const wallEnd =
|
||||
own && trimOfLayouts(layouts) === own.designTrim ? wallSlopeEnd(section, own) : null;
|
||||
if (own && wallEnd !== null) ownRow[`slope_end_${own.side}_m`] = Number(wallEnd.toFixed(4));
|
||||
const pipeRow = Object.keys(ownRow).length > 1 ? ownRow : null;
|
||||
const areaInput = areaInputOf(section, layouts);
|
||||
if (!areaInput) return pipeRow;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
`_Engine_Cad_PlanKinds.py` 가 700줄을 넘어 떼어냄(141-1) — 파일 입출력 없음 · 좌표 = 사업지 m.
|
||||
|
||||
⚠⚠ 짝: `B05_Profile/logic/B05_Profile_PlanFootprint.ts`(route_frame · route_normal · side_is_cut ·
|
||||
slope_end · slope_pieces 와 그 아래 보간 · 촘촘히 · 접힘 버림 · 모서리 둥글림) — 종단 · 횡단
|
||||
편집 중 평면 발자국 · 한쪽만 고치면 화면과 계획평면도가 갈림.
|
||||
slope_end · final_slope_end · slope_pieces 와 그 아래 보간 · 촘촘히 · 접힘 버림 · 모서리 둥글림)
|
||||
— 종단 · 횡단 편집 중 평면 발자국 · 한쪽만 고치면 화면과 계획평면도가 갈림.
|
||||
거울 시험: `resources/tester/test_346_2_plan_footprint_mirror.py`
|
||||
"""
|
||||
|
||||
@@ -86,6 +86,19 @@ def slope_end(
|
||||
return end
|
||||
|
||||
|
||||
def final_slope_end(
|
||||
design: dict[str, Any], samples: list[dict[str, Any]], edge: float, side: str
|
||||
) -> float:
|
||||
"""구조물 처리 뒤 비탈 끝 — 벽 측점은 B06 이 얹은 `slope_end_<쪽>_m`(578-2) · 없으면 설계선 끝.
|
||||
|
||||
짝: `finalSlopeEnd`(TS) — 값은 B06 `STRUCTURE_ROW_KEYS` 가 서버 Node 로 셈해 설계 칸에 얹음.
|
||||
"""
|
||||
stored = design.get(f"slope_end_{side}_m")
|
||||
if isinstance(stored, (int, float)) and not isinstance(stored, bool):
|
||||
return float(stored)
|
||||
return slope_end(design.get("design_line") or [], samples, edge, side)
|
||||
|
||||
|
||||
def _split_runs(
|
||||
points: list[list[float]], flags: list[bool]
|
||||
) -> tuple[list[list[list[float]]], list[list[list[float]]]]:
|
||||
|
||||
@@ -42,10 +42,9 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
|
||||
curve_pieces,
|
||||
route_frame,
|
||||
side_is_cut,
|
||||
slope_end,
|
||||
slope_pieces,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import tick_step_m
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import final_slope_end, tick_step_m
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import (
|
||||
_chunk_route,
|
||||
_hillshade_for_box,
|
||||
@@ -375,13 +374,13 @@ def slope_records(
|
||||
right_edge = float(edges["right"]["offset_m"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
continue
|
||||
line, samples = (design or {}).get("design_line") or [], source.get("samples") or []
|
||||
samples = source.get("samples") or []
|
||||
record: dict[str, Any] = {
|
||||
"chainage_m": float(chainage),
|
||||
"left_edge_m": left_edge,
|
||||
"left_end_m": slope_end(line, samples, left_edge, "left"),
|
||||
"left_end_m": final_slope_end(design, samples, left_edge, "left"),
|
||||
"right_edge_m": right_edge,
|
||||
"right_end_m": slope_end(line, samples, right_edge, "right"),
|
||||
"right_end_m": final_slope_end(design, samples, right_edge, "right"),
|
||||
}
|
||||
for side in ("left", "right"):
|
||||
elevation = edges[side].get("elevation_m")
|
||||
|
||||
@@ -19,9 +19,9 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import ( # noqa: E402
|
||||
final_slope_end,
|
||||
route_frame,
|
||||
side_is_cut,
|
||||
slope_end,
|
||||
slope_pieces,
|
||||
)
|
||||
|
||||
@@ -93,6 +93,9 @@ def _stations() -> list[dict]:
|
||||
"samples": samples,
|
||||
}
|
||||
)
|
||||
# 벽 측점(578-2) — B06 이 얹은 구조물 처리 뒤 비탈 끝이 설계선 끝보다 앞섬
|
||||
stations[5]["design"]["slope_end_left_m"] = 4.2
|
||||
stations[6]["design"]["slope_end_right_m"] = -3.1
|
||||
stations.append({**stations[3], "chainage_m": 70.0, "design": {"road_edges": None}})
|
||||
stations.append({**stations[4], "chainage_m": 90.0, "samples": []}) # 지반 없음
|
||||
return stations
|
||||
@@ -107,13 +110,13 @@ def _py_record(station: dict) -> dict | None:
|
||||
right_edge = float(edges["right"]["offset_m"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return None
|
||||
line, samples = design.get("design_line") or [], station["samples"] or []
|
||||
samples = station["samples"] or []
|
||||
record = {
|
||||
"chainage_m": float(station["chainage_m"]),
|
||||
"left_edge_m": left_edge,
|
||||
"left_end_m": slope_end(line, samples, left_edge, "left"),
|
||||
"left_end_m": final_slope_end(design, samples, left_edge, "left"),
|
||||
"right_edge_m": right_edge,
|
||||
"right_end_m": slope_end(line, samples, right_edge, "right"),
|
||||
"right_end_m": final_slope_end(design, samples, right_edge, "right"),
|
||||
}
|
||||
for side in ("left", "right"):
|
||||
elevation = edges[side].get("elevation_m")
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""578-2 평면 비탈 끝 = 횡단 최종 모양(구조물 처리 뒤)이 땅과 만나는 점.
|
||||
|
||||
- 벽(옹벽) 측점 — `structure.layouts`(서버 Node 묶음 · B06 `STRUCTURE_ROW_KEYS`)가
|
||||
`slope_end_<쪽>_m` 을 얹음 · 벽 앞(성토 위 이음 → 전면 발끝)이 땅과 만나는 자리
|
||||
· 설계선 끝보다 안쪽
|
||||
- 구조물 없는 측점 · 벽 반대쪽 — 키 없음(평면 선 전후 0 차)
|
||||
- 파이썬 짝 `final_slope_end` — 키가 있으면 그 값 · 없으면 설계선 끝
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_578_2_plan_slope_end.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import ( # noqa: E402
|
||||
final_slope_end,
|
||||
slope_end,
|
||||
)
|
||||
from common_util.common_util_calc_runner import BUNDLE, run_calc # noqa: E402
|
||||
|
||||
|
||||
def _ground(offset: float) -> float:
|
||||
"""좌(+)로 내려가는 지반 — 좌 = 쌓기 · 우 = 깎기."""
|
||||
return 100.0 - 0.5 * offset
|
||||
|
||||
|
||||
def _section(chainage: float, wall: bool) -> dict:
|
||||
offsets = [-20.0 + 0.5 * i for i in range(81)]
|
||||
line = []
|
||||
for o in offsets:
|
||||
if -2.5 <= o <= 2.5:
|
||||
z = 100.0
|
||||
elif o > 2.5: # 쌓기 1:1.5 → 10 m 에서 땅
|
||||
z = max(100.0 - (o - 2.5) / 1.5, _ground(o))
|
||||
else: # 깎기 1:1 → -5 m 에서 땅
|
||||
z = min(100.0 + (-2.5 - o), _ground(o))
|
||||
line.append({"offset_m": o, "elevation_m": z})
|
||||
section = {
|
||||
"chainage_m": chainage,
|
||||
"center_x": 0.0,
|
||||
"center_y": chainage,
|
||||
"samples": [{"offset_m": o, "elevation_m": _ground(o), "valid": True} for o in offsets],
|
||||
"design": {
|
||||
"design_elevation_m": 100.0,
|
||||
"section_mode": "right_cut",
|
||||
"road_edges": {
|
||||
"left": {"offset_m": 2.5, "elevation_m": 100.0},
|
||||
"right": {"offset_m": -2.5, "elevation_m": 100.0},
|
||||
},
|
||||
"design_line": line,
|
||||
"cut_area_m2": 1.0,
|
||||
"fill_area_m2": 1.0,
|
||||
},
|
||||
}
|
||||
if wall:
|
||||
section["revetment"] = {
|
||||
"structure_id": "w1",
|
||||
"type_id": "retaining_wall",
|
||||
"name": "옹벽",
|
||||
"start_m": 0.0,
|
||||
"end_m": 20.0,
|
||||
"anchor_m": 10.0,
|
||||
"form": "콘크리트",
|
||||
"height_m": 3.0,
|
||||
"side": "좌",
|
||||
}
|
||||
return section
|
||||
|
||||
|
||||
@pytest.mark.skipif(not BUNDLE.is_file(), reason="design_calc 묶음 없음(npm run build:design-calc)")
|
||||
def test_벽_측점만_비탈_끝을_얹음() -> None:
|
||||
sections = [_section(0.0, False), _section(10.0, True), _section(20.0, False)]
|
||||
results = run_calc(["structure.layouts"], {"detail": {"cross_sections": sections}})
|
||||
assert results is not None, "Node 묶음 실패"
|
||||
rows = {row["chainage_m"]: row for row in results["structure.layouts"]["areas"]}
|
||||
plain = slope_end(sections[1]["design"]["design_line"], sections[1]["samples"], 2.5, "left")
|
||||
assert plain == pytest.approx(10.0)
|
||||
end = rows[10.0]["slope_end_left_m"]
|
||||
assert 2.5 < end < plain, f"벽이 자른 끝({end})이 설계선 끝({plain}) 안쪽이어야"
|
||||
assert "slope_end_right_m" not in rows[10.0], "벽 반대쪽은 그대로"
|
||||
for chainage in (0.0, 20.0):
|
||||
assert not any(key.startswith("slope_end_") for key in rows.get(chainage, {}))
|
||||
|
||||
|
||||
def test_파이썬_짝은_얹은_값을_먼저() -> None:
|
||||
section = _section(10.0, False)
|
||||
design, samples = section["design"], section["samples"]
|
||||
assert final_slope_end(design, samples, 2.5, "left") == pytest.approx(10.0)
|
||||
assert final_slope_end({**design, "slope_end_left_m": 4.25}, samples, 2.5, "left") == 4.25
|
||||
assert final_slope_end({**design, "slope_end_left_m": None}, samples, 2.5, "left") == (
|
||||
pytest.approx(10.0)
|
||||
)
|
||||
Reference in New Issue
Block a user