main_laptop_1 -> main byeonghap (4 hwangyeong 585 commits) #12
@@ -475,6 +475,9 @@ export interface CrossDesign {
|
||||
rock_boundary_offset_m?: number;
|
||||
/** 측점 개별 표시 반폭(m, 2026-08-06). 확정 시 병합되며 세션 값이 우선이다. */
|
||||
display_half_width_m?: number;
|
||||
/** 이 측점에 놓인 소단 제원(계획서 3-9). 없으면 계단 없음.
|
||||
* 세션이 비어도(확정 뒤·다른 PC) 저장분만으로 계단이 서게 되싣는 값이다. */
|
||||
berm?: { width_m: number; interval_m: number; slope_deg: number };
|
||||
}
|
||||
|
||||
export interface CrossDesignResponse {
|
||||
|
||||
@@ -38,6 +38,7 @@ from B06_Section.B06_Section_Engine_Areas import (
|
||||
from common_util.common_util_cross_berm import (
|
||||
BermSpec,
|
||||
cut_profile_points,
|
||||
fill_profile_points,
|
||||
)
|
||||
from common_util.common_util_cross_berm import elevation_at as berm_elevation_at
|
||||
from config.config_system import (
|
||||
@@ -196,6 +197,7 @@ class _SectionGeometry:
|
||||
# 소단 제원(없으면 None) — 절토 사면 꼭짓점 셈에 그대로 넘어간다.
|
||||
self.berm = berm
|
||||
self._cut_points_cache: dict[str, list[tuple[float, float]]] = {}
|
||||
self._fill_points_cache: dict[str, list[tuple[float, float]]] = {}
|
||||
# 절토 사면·지반 최초 교차거리(측별 캐시) — 교차 후 절토 종료용(N-2-4).
|
||||
self._cut_cross: dict[str, float | None] = {}
|
||||
self._fill_cross: dict[str, float | None] = {}
|
||||
@@ -313,6 +315,19 @@ class _SectionGeometry:
|
||||
"""절토 사면선 표고(무릎·소단 반영). 지반 교차 클램프는 하지 않는다."""
|
||||
return berm_elevation_at(self.cut_points(side), dist)
|
||||
|
||||
def fill_points(self, side: str) -> list[tuple[float, float]]:
|
||||
"""성토 사면 꼭짓점 — 소단이 들어 있다. 절토와 달리 무릎은 없다."""
|
||||
if side in self._fill_points_cache:
|
||||
return self._fill_points_cache[side]
|
||||
start_dist, start_z = self._slope_start(side)
|
||||
points = fill_profile_points(start_dist, start_z, self.fill_ratio, self.berm)
|
||||
self._fill_points_cache[side] = points
|
||||
return points
|
||||
|
||||
def _fill_slope_z(self, side: str, dist: float) -> float:
|
||||
"""성토 사면선 표고(소단 반영). 지반 교차 클램프는 하지 않는다."""
|
||||
return berm_elevation_at(self.fill_points(side), dist)
|
||||
|
||||
def cut_slope_segments(self) -> list[dict[str, Any]]:
|
||||
"""절토 사면을 **경사 구간별로** 쪼갠 목록 — 법정 경사 검사가 읽는 값이다.
|
||||
|
||||
@@ -445,7 +460,7 @@ class _SectionGeometry:
|
||||
max_dist = start_dist + 500.0
|
||||
while dist <= max_dist:
|
||||
signed = dist if side == "left" else -dist
|
||||
fill_line = start_z - (dist - start_dist) / self.fill_ratio
|
||||
fill_line = self._fill_slope_z(side, dist)
|
||||
if fill_line - self._ground_at(signed) <= 0:
|
||||
result = dist
|
||||
break
|
||||
@@ -477,9 +492,7 @@ class _SectionGeometry:
|
||||
return z0 + (z1 - z0) * ratio
|
||||
return points[-1][1]
|
||||
role = self.left_role if side == "left" else self.right_role
|
||||
start_dist, start_z = self._slope_start(side)
|
||||
dist = abs(offset_m)
|
||||
run = dist - start_dist
|
||||
if role == "cut":
|
||||
# 지반과 1회 교차하면 그 이후 절토는 의미 없음 → 지반 추종(N-2-4).
|
||||
cross = self.cut_cross_dist(side)
|
||||
@@ -490,8 +503,7 @@ class _SectionGeometry:
|
||||
cross = self.fill_cross_dist(side)
|
||||
if cross is not None and dist >= cross:
|
||||
return ground_m
|
||||
fill_line = start_z - run / self.fill_ratio
|
||||
return max(fill_line, ground_m)
|
||||
return max(self._fill_slope_z(side, dist), ground_m)
|
||||
|
||||
def breakpoints(self) -> list[float]:
|
||||
"""적분·설계선에 반드시 포함할 설계 꼭짓점 오프셋 목록(2단계 무릎·소단 포함)."""
|
||||
@@ -508,6 +520,17 @@ class _SectionGeometry:
|
||||
if cross is not None and offset > cross + 1e-9:
|
||||
break # 지반과 만난 뒤는 절토가 없다
|
||||
points.append(offset if side == "left" else -offset)
|
||||
# 성토 사면 소단 모서리 — 절토와 같은 까닭으로 설계선에 실어야 계단이 그려진다.
|
||||
if self.berm is not None:
|
||||
for side in ("left", "right"):
|
||||
role = self.left_role if side == "left" else self.right_role
|
||||
if role != "fill":
|
||||
continue
|
||||
cross = self.fill_cross_dist(side)
|
||||
for offset, _z in self.fill_points(side):
|
||||
if cross is not None and offset > cross + 1e-9:
|
||||
break
|
||||
points.append(offset if side == "left" else -offset)
|
||||
# 절·성토 사면과 지반의 **첫** 교차점을 꼭짓점에 넣어 면적 절단을 정확히 한다(N-2-4).
|
||||
for side in ("left", "right"):
|
||||
role = self.left_role if side == "left" else self.right_role
|
||||
|
||||
@@ -103,13 +103,60 @@ export function fillSlopeLengths(section: CrossSection): {
|
||||
const meet = meetOffset(designAt, groundAt, start, limit, outward);
|
||||
const end = Math.abs(meet) > Math.abs(limit) ? limit : meet;
|
||||
lengths[side] = {
|
||||
lengthM: Math.abs(end - start) * slant,
|
||||
// 소단이 있으면 사면이 계단으로 끊기므로 **구간별 최대**를 잰다. 없으면 종전대로
|
||||
// 수평 성분 × 기울기 — 값이 한 톨도 안 바뀐다(2026-09-07, 계획서 3-9).
|
||||
lengthM: design.berm
|
||||
? longestFillRun(design, start, end, slant)
|
||||
: Math.abs(end - start) * slant,
|
||||
open: Math.abs(designAt(end) - groundAt(end)) > MEET_TOLERANCE_M,
|
||||
};
|
||||
}
|
||||
return lengths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 소단으로 끊긴 성토사면에서 **가장 긴 한 구간**의 경사길이(m).
|
||||
*
|
||||
* 왜 최대인가 — 법정 기준은 「성토사면 길이 5m 이내, 넘으면 옹벽·석축 의무」다
|
||||
* (`성토_비탈면.md` §2). 소단은 사면을 끊는 시설이므로, 판정은 **끊긴 한 도막**을 봐야 한다.
|
||||
* 전체를 한 줄로 재면 소단을 넣어도 5m 를 넘어 **의무가 사라지지 않고**, 반대로 실효 경사로
|
||||
* 재면 완만해져 **의무가 사라진 것처럼** 보인다. 둘 다 틀린다.
|
||||
*
|
||||
* 평탄부(소단)는 길이에 안 넣고 그 자리에서 도막을 끊는다.
|
||||
*/
|
||||
function longestFillRun(
|
||||
design: CrossDesign,
|
||||
startOffset: number,
|
||||
endOffset: number,
|
||||
slant: number,
|
||||
): number {
|
||||
const berm = design.berm;
|
||||
if (!berm) return Math.abs(endOffset - startOffset) * slant;
|
||||
const low = Math.min(startOffset, endOffset);
|
||||
const high = Math.max(startOffset, endOffset);
|
||||
const bermRise = Math.tan((berm.slope_deg * Math.PI) / 180) * berm.width_m;
|
||||
let longest = 0;
|
||||
let current = 0;
|
||||
const line = design.design_line;
|
||||
for (let index = 1; index < line.length; index += 1) {
|
||||
const a = line[index - 1];
|
||||
const b = line[index];
|
||||
const from = Math.max(Math.min(a.offset_m, b.offset_m), low);
|
||||
const to = Math.min(Math.max(a.offset_m, b.offset_m), high);
|
||||
if (to - from <= 1e-9) continue; // 이 도막은 사면 밖이다
|
||||
const run = Math.abs(b.offset_m - a.offset_m);
|
||||
const rise = b.elevation_m - a.elevation_m;
|
||||
const isBerm = Math.abs(run - berm.width_m) < 1e-6 && Math.abs(rise - bermRise) < 1e-6;
|
||||
if (isBerm) {
|
||||
longest = Math.max(longest, current);
|
||||
current = 0;
|
||||
continue;
|
||||
}
|
||||
current += (to - from) * slant;
|
||||
}
|
||||
return Math.max(longest, current);
|
||||
}
|
||||
|
||||
/** 노면·노견(+측구) 구간의 바깥 경계 — 교차점 탐색은 여기서부터 바깥으로 간다. */
|
||||
function protectedSpan(
|
||||
design: CrossDesign,
|
||||
|
||||
@@ -132,6 +132,52 @@ def cut_profile_points(
|
||||
return _dedupe(points)
|
||||
|
||||
|
||||
def fill_profile_points(
|
||||
start_dist: float,
|
||||
start_z: float,
|
||||
fill_ratio: float,
|
||||
berm: BermSpec | None,
|
||||
max_reach_m: float = _MAX_REACH_M,
|
||||
) -> list[tuple[float, float]]:
|
||||
"""성토 사면 꼭짓점 `[(거리, 표고), ...]` — 사면 시작에서 바깥으로 **내려간다**.
|
||||
|
||||
절토와 달리 경사가 하나뿐이라 무릎이 없다(암 경계는 절토만 본다). 소단은 같은 규칙 —
|
||||
사면길이가 `interval_m` 에 닿을 때마다 폭 `width_m` 의 평탄부를 넣고, 그 평탄부는
|
||||
안쪽이 낮도록 `slope_deg` 만큼 기울어 **바깥으로 갈수록 조금 올라간다**(물이 노면 쪽으로).
|
||||
"""
|
||||
points: list[tuple[float, float]] = [(start_dist, start_z)]
|
||||
dist, elevation = start_dist, start_z
|
||||
slant_since_berm = 0.0
|
||||
limit = start_dist + max_reach_m
|
||||
berm_rise = (
|
||||
math.tan(math.radians(berm.slope_deg)) * berm.width_m
|
||||
if berm is not None and berm.width_m > 0
|
||||
else 0.0
|
||||
)
|
||||
ratio = max(fill_ratio, 1e-6)
|
||||
|
||||
while dist < limit:
|
||||
drop = _STEP_M / ratio
|
||||
slant = math.hypot(_STEP_M, drop)
|
||||
if berm is not None and berm.interval_m > 0 and slant_since_berm + slant >= berm.interval_m:
|
||||
remain = max(berm.interval_m - slant_since_berm, 0.0)
|
||||
run = remain / math.hypot(1.0, 1.0 / ratio)
|
||||
dist += run
|
||||
elevation -= run / ratio
|
||||
points.append((dist, elevation)) # 소단 안쪽 모서리
|
||||
dist += berm.width_m
|
||||
elevation += berm_rise
|
||||
points.append((dist, elevation)) # 소단 바깥 모서리
|
||||
slant_since_berm = 0.0
|
||||
continue
|
||||
dist += _STEP_M
|
||||
elevation -= drop
|
||||
slant_since_berm += slant
|
||||
|
||||
points.append((dist, elevation))
|
||||
return _dedupe(points)
|
||||
|
||||
|
||||
def _dedupe(points: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
||||
"""같은 자리 꼭짓점을 지운다 — 보간이 0 나눗셈을 만나지 않게."""
|
||||
out: list[tuple[float, float]] = []
|
||||
|
||||
@@ -122,6 +122,51 @@ export function cutProfilePoints(
|
||||
return dedupe(points);
|
||||
}
|
||||
|
||||
/**
|
||||
* 짝: `fill_profile_points`. 성토 사면 꼭짓점 — 시작에서 바깥으로 **내려간다**.
|
||||
*
|
||||
* 절토와 달리 경사가 하나뿐이라 무릎이 없다. 소단 규칙은 같다.
|
||||
*/
|
||||
export function fillProfilePoints(
|
||||
startDist: number,
|
||||
startZ: number,
|
||||
fillRatio: number,
|
||||
berm: BermSpec | null,
|
||||
maxReachM: number = MAX_REACH_M,
|
||||
): Array<[number, number]> {
|
||||
const points: Array<[number, number]> = [[startDist, startZ]];
|
||||
let dist = startDist;
|
||||
let elevation = startZ;
|
||||
let slantSinceBerm = 0;
|
||||
const limit = startDist + maxReachM;
|
||||
const bermRise =
|
||||
berm !== null && berm.widthM > 0 ? Math.tan((berm.slopeDeg * Math.PI) / 180) * berm.widthM : 0;
|
||||
const ratio = Math.max(fillRatio, 1e-6);
|
||||
|
||||
while (dist < limit) {
|
||||
const drop = STEP_M / ratio;
|
||||
const slant = Math.hypot(STEP_M, drop);
|
||||
if (berm !== null && berm.intervalM > 0 && slantSinceBerm + slant >= berm.intervalM) {
|
||||
const remain = Math.max(berm.intervalM - slantSinceBerm, 0);
|
||||
const run = remain / Math.hypot(1, 1 / ratio);
|
||||
dist += run;
|
||||
elevation -= run / ratio;
|
||||
points.push([dist, elevation]); // 소단 안쪽 모서리
|
||||
dist += berm.widthM;
|
||||
elevation += bermRise;
|
||||
points.push([dist, elevation]); // 소단 바깥 모서리
|
||||
slantSinceBerm = 0;
|
||||
continue;
|
||||
}
|
||||
dist += STEP_M;
|
||||
elevation -= drop;
|
||||
slantSinceBerm += slant;
|
||||
}
|
||||
|
||||
points.push([dist, elevation]);
|
||||
return dedupe(points);
|
||||
}
|
||||
|
||||
/** 같은 자리 꼭짓점을 지운다 — 보간이 0 나눗셈을 만나지 않게. */
|
||||
function dedupe(points: Array<[number, number]>): Array<[number, number]> {
|
||||
const out: Array<[number, number]> = [];
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
* `common_util_cross_design.ts` 가 700줄을 넘어 떼어냈다(2026-09-04) — 계산은 그대로다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { type BermSpec, cutProfilePoints, elevationAt } from "./common_util_cross_berm";
|
||||
import {
|
||||
type BermSpec,
|
||||
cutProfilePoints,
|
||||
elevationAt,
|
||||
fillProfilePoints,
|
||||
} from "./common_util_cross_berm";
|
||||
|
||||
/** 절토 사면 경사 구간 한 칸 — 짝 파이썬 `cut_slope_segments` 와 같은 항목. */
|
||||
export interface CutSlopeSegment {
|
||||
@@ -107,6 +112,7 @@ export class SectionGeometry {
|
||||
/** 소단 제원(없으면 null) — 절토 사면 꼭짓점 셈에 그대로 넘어간다. */
|
||||
berm: BermSpec | null = null;
|
||||
private cutPointsCache = new Map<string, Array<[number, number]>>();
|
||||
private fillPointsCache = new Map<string, Array<[number, number]>>();
|
||||
private cutCross = new Map<string, number | null>();
|
||||
private fillCross = new Map<string, number | null>();
|
||||
|
||||
@@ -251,6 +257,21 @@ export class SectionGeometry {
|
||||
return elevationAt(this.cutPoints(side), dist);
|
||||
}
|
||||
|
||||
/** 짝: `fill_points`. 성토 사면 꼭짓점 — 소단이 들어 있다(무릎은 없다). */
|
||||
fillPoints(side: string): Array<[number, number]> {
|
||||
const cached = this.fillPointsCache.get(side);
|
||||
if (cached !== undefined) return cached;
|
||||
const [startDist, startZ] = this.slopeStart(side);
|
||||
const points = fillProfilePoints(startDist, startZ, this.fillRatio, this.berm);
|
||||
this.fillPointsCache.set(side, points);
|
||||
return points;
|
||||
}
|
||||
|
||||
/** 짝: `_fill_slope_z`. 성토 사면선 표고(소단 반영, 지반 클램프 없음). */
|
||||
private fillSlopeZ(side: string, dist: number): number {
|
||||
return elevationAt(this.fillPoints(side), dist);
|
||||
}
|
||||
|
||||
/** 짝: `cut_cross_dist`. 절토 사면이 지반선과 처음 만나는 거리(N-2-4). */
|
||||
cutCrossDist(side: string): number | null {
|
||||
const cached = this.cutCross.get(side);
|
||||
@@ -279,12 +300,12 @@ export class SectionGeometry {
|
||||
if (cached !== undefined) return cached;
|
||||
let result: number | null = null;
|
||||
if (this.groundAt !== null) {
|
||||
const [startDist, startZ] = this.slopeStart(side);
|
||||
const [startDist] = this.slopeStart(side);
|
||||
let dist = startDist;
|
||||
const maxDist = startDist + CROSS_MAX_M;
|
||||
while (dist <= maxDist) {
|
||||
const signed = side === "left" ? dist : -dist;
|
||||
const fillLine = startZ - (dist - startDist) / this.fillRatio;
|
||||
const fillLine = this.fillSlopeZ(side, dist);
|
||||
if (fillLine - this.groundAt(signed) <= 0) {
|
||||
result = dist;
|
||||
break;
|
||||
@@ -339,9 +360,7 @@ export class SectionGeometry {
|
||||
}
|
||||
}
|
||||
const role = side === "left" ? this.leftRole : this.rightRole;
|
||||
const [startDist, startZ] = this.slopeStart(side);
|
||||
const dist = Math.abs(offsetM);
|
||||
const run = dist - startDist;
|
||||
if (role === "cut") {
|
||||
const cross = this.cutCrossDist(side);
|
||||
if (cross !== null && dist >= cross) return groundM;
|
||||
@@ -349,7 +368,7 @@ export class SectionGeometry {
|
||||
}
|
||||
const cross = this.fillCrossDist(side);
|
||||
if (cross !== null && dist >= cross) return groundM;
|
||||
return Math.max(startZ - run / this.fillRatio, groundM);
|
||||
return Math.max(this.fillSlopeZ(side, dist), groundM);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,6 +438,18 @@ export class SectionGeometry {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 성토 사면 소단 모서리 — 절토와 같은 까닭으로 설계선에 실어야 계단이 그려진다.
|
||||
if (this.berm !== null) {
|
||||
for (const side of ["left", "right"]) {
|
||||
const role = side === "left" ? this.leftRole : this.rightRole;
|
||||
if (role !== "fill") continue;
|
||||
const cross = this.fillCrossDist(side);
|
||||
for (const [offset] of this.fillPoints(side)) {
|
||||
if (cross !== null && offset > cross + 1e-9) break;
|
||||
points.push(side === "left" ? offset : -offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const side of ["left", "right"]) {
|
||||
const role = side === "left" ? this.leftRole : this.rightRole;
|
||||
const cross = role === "cut" ? this.cutCrossDist(side) : this.fillCrossDist(side);
|
||||
|
||||
Reference in New Issue
Block a user