Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -238,3 +238,27 @@ export function carryEdits(
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 꼭짓점의 초기값 직선 열쇠(432) — 같은 자리면 옛 꼭짓점 열쇠(꼭짓점 위일 때만) · 바뀐 자리면 새
|
||||
* 지반 첫 계획선 열쇠(파이썬 `carry_no_curve_keys` 짝). 초기값만 · 그 뒤는 사용자 몫(요구 262).
|
||||
*/
|
||||
export function carryNoCurveKeys(
|
||||
oldXy: ReadonlyArray<XY>,
|
||||
newXy: ReadonlyArray<XY>,
|
||||
oldKeys: ReadonlyArray<string>,
|
||||
groundKeys: ReadonlyArray<string>,
|
||||
groundPvi: ReadonlyArray<AlignmentNode>,
|
||||
tolM: number = SAME_LINE_TOL_M,
|
||||
): string[] {
|
||||
const match = lineMatcher(oldXy, newXy, tolM);
|
||||
const old = oldKeys.map(Number);
|
||||
const ground = new Set(groundKeys);
|
||||
return groundPvi
|
||||
.filter((node) => {
|
||||
const s = match(node.chainage_m);
|
||||
if (s === null) return ground.has(node.chainage_m.toFixed(3));
|
||||
return old.some((value) => Math.abs(s - value) < ON_NODE_M);
|
||||
})
|
||||
.map((node) => node.chainage_m.toFixed(3));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type AlignmentNode,
|
||||
type AlignmentPolicy,
|
||||
} from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { initialNoCurveKeys } from "./B05_Profile_Profile_CurveLaw";
|
||||
|
||||
/** 종단 지반 표본 한 점 — 저장 종단 `samples[]` 꼴. */
|
||||
export interface GroundSample {
|
||||
@@ -83,6 +84,16 @@ export function passThrough(baseS: number[], target: number[], policy: Alignment
|
||||
|
||||
/** 첫 계획선 꼭짓점(base_pvi · 0.000001 m) — 못 서는 땅이면 null(서버로 다시 셈). */
|
||||
export function groundFollowingBasePvi(input: GroundFollowingInput): AlignmentNode[] | null {
|
||||
return groundFollowingPlan(input)?.pvi ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 첫 계획선 꼭짓점 + 초기값 직선 열쇠(432) — 기본 L 기준 R > 500 m 꼭짓점은 곡선 없이(꼭짓점 =
|
||||
* 지반고 그대로) · 파이썬 `design_ground_following_profile` 의 `no_curve_keys` 짝.
|
||||
*/
|
||||
export function groundFollowingPlan(
|
||||
input: GroundFollowingInput,
|
||||
): { pvi: AlignmentNode[]; noCurveKeys: string[] } | null {
|
||||
const profile = groundProfile(input.samples);
|
||||
if (!profile) return null;
|
||||
const { chainage, ground } = profile;
|
||||
@@ -103,9 +114,14 @@ export function groundFollowingBasePvi(input: GroundFollowingInput): AlignmentNo
|
||||
const target = nodeS.map((s) => interp(chainage, ground, s));
|
||||
target[0] += input.startOffsetM ?? 0;
|
||||
target[target.length - 1] += input.endOffsetM ?? 0;
|
||||
const baseZ = passThrough(nodeS, target, input.policy);
|
||||
return nodeS.map((s, index) => ({
|
||||
const noCurveKeys = initialNoCurveKeys(
|
||||
nodeS.map((s, index) => ({ chainage_m: s, elevation_m: target[index] })),
|
||||
input.policy.default_curve_length_m,
|
||||
);
|
||||
const baseZ = passThrough(nodeS, target, { ...input.policy, no_curve_keys: noCurveKeys });
|
||||
const pvi = nodeS.map((s, index) => ({
|
||||
chainage_m: roundTo(s, 6),
|
||||
elevation_m: roundTo(baseZ[index], 6),
|
||||
}));
|
||||
return { pvi, noCurveKeys };
|
||||
}
|
||||
|
||||
@@ -32,9 +32,16 @@ import {
|
||||
type AlignmentBase,
|
||||
type AlignmentEdits,
|
||||
} from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { carryBase, carryEdits, changedSpan, type XY } from "./B05_Profile_Grade_Carry";
|
||||
import {
|
||||
carryBase,
|
||||
carryEdits,
|
||||
carryNoCurveKeys,
|
||||
changedSpan,
|
||||
type XY,
|
||||
} from "./B05_Profile_Grade_Carry";
|
||||
import { SECTION_STATION_INTERVAL_M } from "./B05_Profile_Sections_Stations";
|
||||
import { groundFollowingBasePvi, groundProfile } from "./B05_Profile_Grade_Ground";
|
||||
import { groundFollowingPlan, groundProfile } from "./B05_Profile_Grade_Ground";
|
||||
import { DEFAULT_CURVE_LENGTH_M } from "./B05_Profile_Profile_CurveLaw";
|
||||
|
||||
const xy = (line: ReadonlyArray<Vertex>): Array<{ x: number; y: number }> =>
|
||||
line.map(([x, y]) => ({ x, y }));
|
||||
@@ -165,17 +172,30 @@ export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
||||
}));
|
||||
const profile = groundProfile(samples);
|
||||
if (!profile) return saved; // 못 서는 땅 — 저장 기준 그대로(서버가 [저장] 때 다시 셈)
|
||||
const groundPvi =
|
||||
groundFollowingBasePvi({
|
||||
samples,
|
||||
stations: ground.stations.map((station) => station.chainageM),
|
||||
policy: saved.policy,
|
||||
}) ?? [];
|
||||
// 바뀐 자리 새 꼭짓점 = 서버 새 지반 첫 계획선(기본 L = config 짝 · 초기값 직선 열쇠 · 432)
|
||||
const plan = groundFollowingPlan({
|
||||
samples,
|
||||
stations: ground.stations.map((station) => station.chainageM),
|
||||
policy: { ...saved.policy, default_curve_length_m: DEFAULT_CURVE_LENGTH_M },
|
||||
});
|
||||
const groundPvi = plan?.pvi ?? [];
|
||||
const savedLine = deps.savedLine();
|
||||
const span = savedLine ? changedSpan(xy(savedLine), xy(route.line)) : null;
|
||||
const savedPvi = deps.storedPvi?.() ?? saved.basePvi;
|
||||
return {
|
||||
policy: saved.policy,
|
||||
// 이어받은 구간 = 옛 정책(L · 직선 열쇠) · 바뀐 자리 새 꼭짓점만 새 열쇠(서버 `carry_policy` 짝)
|
||||
policy: span
|
||||
? {
|
||||
...saved.policy,
|
||||
no_curve_keys: carryNoCurveKeys(
|
||||
xy(savedLine!),
|
||||
xy(route.line),
|
||||
saved.policy.no_curve_keys ?? [],
|
||||
plan?.noCurveKeys ?? [],
|
||||
groundPvi,
|
||||
),
|
||||
}
|
||||
: saved.policy,
|
||||
onlyExplicitCurves: saved.onlyExplicitCurves,
|
||||
basePvi: span ? carryBase(xy(savedLine!), xy(route.line), savedPvi, groundPvi) : savedPvi,
|
||||
chainage: profile.chainage,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_Profile_CurveLaw.ts
|
||||
* 종단곡선 초기값 직선 · 법정 미달 판정(432 · 요구 262).
|
||||
*
|
||||
* 짝: 파이썬 `B05_Profile_Engine_Grade_CurveLaw.py`(`initial_no_curve_keys` ·
|
||||
* `vertical_curve_shortfalls`) · config `FOREST_ROAD_PROFILE_ALIGNMENT` 의 `default_curve_length_m` ·
|
||||
* `initial_straight_radius_m` — 거울 시험 `test_432_1_curve_law.py`.
|
||||
*
|
||||
* - 초기값에서만 기본 L 기준 R > 500 m(|대수차| < L/500) 꼭짓점은 곡선 없음 — 정책 `no_curve_keys`
|
||||
* 로 얼려 두고, 사용자가 그 꼭짓점을 고치거나(source "user") R 을 넣으면 다시 기본 곡선.
|
||||
* - 법정 최소 R · L 미달은 막지 않고 경고 글만 · 비포장 대수차 5 % 이하(생략 가능)는 뺌.
|
||||
* ========================================================================== */
|
||||
|
||||
import type {
|
||||
AlignmentCurve,
|
||||
AlignmentNode,
|
||||
AlignmentPolicy,
|
||||
} from "./B05_Profile_UI_Profile_Alignment";
|
||||
|
||||
/** 기본 종단곡선 길이 L(m) — config `default_curve_length_m` 짝(새 정책에 담겨 옴 · 옛 저장분은 저장값). */
|
||||
export const DEFAULT_CURVE_LENGTH_M = 20;
|
||||
/** 초기값 직선 문턱 R(m) — config `initial_straight_radius_m` 짝. */
|
||||
export const INITIAL_STRAIGHT_RADIUS_M = 500;
|
||||
|
||||
/** 안쪽 꼭짓점 중 기본 L 기준 R > 500 m 인 것의 열쇠(소수 3자리) — 파이썬 `initial_no_curve_keys`. */
|
||||
export function initialNoCurveKeys(
|
||||
nodes: ReadonlyArray<AlignmentNode>,
|
||||
curveLengthM: number | null | undefined,
|
||||
): string[] {
|
||||
if (!curveLengthM || nodes.length < 3) return [];
|
||||
const limit = curveLengthM / INITIAL_STRAIGHT_RADIUS_M;
|
||||
const keys: string[] = [];
|
||||
for (let index = 1; index < nodes.length - 1; index += 1) {
|
||||
const [a, b, c] = [nodes[index - 1], nodes[index], nodes[index + 1]];
|
||||
const spanIn = b.chainage_m - a.chainage_m;
|
||||
const spanOut = c.chainage_m - b.chainage_m;
|
||||
const gradeIn = (b.elevation_m - a.elevation_m) / (spanIn > 0 ? spanIn : 1);
|
||||
const gradeOut = (c.elevation_m - b.elevation_m) / (spanOut > 0 ? spanOut : 1);
|
||||
if (Math.abs(gradeOut - gradeIn) < limit) keys.push(b.chainage_m.toFixed(3));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/** 초기값 직선 꼭짓점이라 곡선을 안 넣는가 — 사용자가 고친 꼭짓점 · R 지정이면 넣음. */
|
||||
export function skipsInitialCurve(
|
||||
policy: AlignmentPolicy,
|
||||
key: string,
|
||||
node: AlignmentNode & { source?: string },
|
||||
hasRequested: boolean,
|
||||
): boolean {
|
||||
return !hasRequested && node.source !== "user" && (policy.no_curve_keys ?? []).includes(key);
|
||||
}
|
||||
|
||||
/** 법정 최소 R · L 을 못 채운 종단곡선 경고 글 — 파이썬 `vertical_curve_shortfalls` 와 같은 글. */
|
||||
export function verticalCurveShortfalls(
|
||||
curves: ReadonlyArray<AlignmentCurve>,
|
||||
policy: AlignmentPolicy,
|
||||
): string[] {
|
||||
const minR = policy.min_vertical_radius_m ?? 0;
|
||||
const minL = policy.min_curve_length_m ?? 0;
|
||||
if (minR <= 0 && minL <= 0) return [];
|
||||
return curves
|
||||
.filter((curve) => !curve.omitted && !curve.skip_allowed)
|
||||
.filter((curve) => curve.r_m < minR - 1e-6 || curve.l_m < minL - 1e-6)
|
||||
.map(
|
||||
(curve) =>
|
||||
`${curve.chainage_m.toFixed(1)}m: 종단곡선 R ${curve.r_m.toFixed(0)}m · L ${curve.l_m.toFixed(1)}m — ` +
|
||||
`법정 최소 R ${minR.toFixed(0)}m · L ${minL.toFixed(0)}m 미달`,
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import { pivotAxes } from "./B05_Profile_UI_Profile_Pivot";
|
||||
import { isStraightBend } from "./B05_Profile_Profile_Straight";
|
||||
import { skipsInitialCurve, verticalCurveShortfalls } from "./B05_Profile_Profile_CurveLaw";
|
||||
|
||||
export interface AlignmentPolicy {
|
||||
station_interval_m: number;
|
||||
@@ -35,6 +36,10 @@ export interface AlignmentPolicy {
|
||||
min_plan_radius_m?: number;
|
||||
/** 배향곡선 하한(m) — 이보다 급하면 경고만 낸다(2026-09-06 사용자 확정). */
|
||||
hairpin_min_radius_m?: number;
|
||||
/** 종단곡선 법정 최소 R · L(m) · 초기값 곡선 없는 꼭짓점 열쇠(432 · `_Profile_CurveLaw`). */
|
||||
min_vertical_radius_m?: number;
|
||||
min_curve_length_m?: number;
|
||||
no_curve_keys?: string[];
|
||||
}
|
||||
|
||||
export interface AlignmentNode {
|
||||
@@ -316,6 +321,7 @@ export function buildCurves(
|
||||
const hasRequested = Number.isFinite(requested) && requested > 0;
|
||||
// 전체 측점 폴리라인 — 사용자가 R을 지정한 변화점에만 라운드가 생긴다.
|
||||
if (onlyExplicit && !hasRequested) continue;
|
||||
if (skipsInitialCurve(policy, key, pvi[index], hasRequested)) continue; // 초기값 직선(432)
|
||||
const fallbackLength = Number.isFinite(policy.default_curve_length_m)
|
||||
? (policy.default_curve_length_m as number)
|
||||
: policy.default_curve_radius_m * Math.abs(delta);
|
||||
@@ -423,6 +429,7 @@ export function buildAlignment(raw: AlignmentBase, input: AlignmentEdits): Profi
|
||||
grade_percent: length > 0 ? (height / length) * 100 : 0,
|
||||
});
|
||||
}
|
||||
warnings.push(...verticalCurveShortfalls(curves, base.policy)); // 법정 R · L 미달(432)
|
||||
curves.forEach((curve) => {
|
||||
curve.bvc_elevation_m = evaluateAt(pviS, pviZ, curves, curve.bvc_m);
|
||||
curve.evc_elevation_m = evaluateAt(pviS, pviZ, curves, curve.evc_m);
|
||||
|
||||
Reference in New Issue
Block a user