Merge remote-tracking branch 'origin/dev' into sub_laptop_4
This commit is contained in:
@@ -18,6 +18,8 @@ from config.config_system import FOREST_ROAD_PROFILE_ALIGNMENT
|
||||
|
||||
# 초기값 직선 문턱 R(m) — TS `INITIAL_STRAIGHT_RADIUS_M` 과 같은 값.
|
||||
INITIAL_STRAIGHT_RADIUS_M = float(FOREST_ROAD_PROFILE_ALIGNMENT["initial_straight_radius_m"])
|
||||
# 법정 미달 판정 오차(m) — R = L/Δ 되셈 L 19.99998 거짓 미달 막음(271-1) · TS `SHORTFALL_TOLERANCE_M` 짝.
|
||||
SHORTFALL_TOLERANCE_M = 1e-3
|
||||
|
||||
|
||||
def flat_bends(base_s: np.ndarray, base_z: np.ndarray, curve_length_m: float | None) -> np.ndarray:
|
||||
@@ -60,7 +62,10 @@ def vertical_curve_shortfalls(
|
||||
continue
|
||||
radius = float(curve.get("radius_m", curve.get("r_m", 0.0)))
|
||||
length = float(curve.get("length_m", curve.get("l_m", 0.0)))
|
||||
if radius < min_radius_m - 1e-6 or length < min_length_m - 1e-6:
|
||||
if (
|
||||
radius < min_radius_m - SHORTFALL_TOLERANCE_M
|
||||
or length < min_length_m - SHORTFALL_TOLERANCE_M
|
||||
):
|
||||
out.append(
|
||||
f"{float(curve['chainage_m']):.1f}m: 종단곡선 R {radius:.0f}m · L {length:.1f}m — "
|
||||
f"법정 최소 R {min_radius_m:.0f}m · L {min_length_m:.0f}m 미달"
|
||||
|
||||
@@ -29,11 +29,13 @@ import {
|
||||
hasStraightRuns,
|
||||
shiftStraightRuns,
|
||||
straightenBetween,
|
||||
straightSubRun,
|
||||
tiltStraightRun,
|
||||
} from "./logic/B05_Profile_UI_Profile_Straighten";
|
||||
import type { IrregularStation } from "./B05_Profile_UI_IrregularStations";
|
||||
import { createFreeDraw } from "../A00_Common/design/logic/design_profile_free";
|
||||
import { createFreeDraw, snapStation } from "../A00_Common/design/logic/design_profile_free";
|
||||
import { createFreeView, type FreeChartGeom } from "../A00_Common/design/design_profile_free_view";
|
||||
import { createRangeBandView } from "../A00_Common/design/design_profile_range_band";
|
||||
import { arrowsBelongToProfile } from "../B06_Section/B06_Section_UI_Cross_Panel_Base";
|
||||
import { stepStructureStation } from "../common_util/common_util_structure_station";
|
||||
import { draftProjectId, useDesignStore } from "../A00_Common/design/design_store";
|
||||
@@ -121,17 +123,37 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
alignment: ctx.alignment,
|
||||
edits: ctx.edits,
|
||||
apply: (next) => ctx.applyEdits(next),
|
||||
autoRadii: ctx.savedRadii,
|
||||
close: () => tools.clear(),
|
||||
changed: ctx.refresh,
|
||||
});
|
||||
const freeView = createFreeView(free);
|
||||
let lastGeom: FreeChartGeom | null = null;
|
||||
// [직선화] 실시간 띠(271-2) — 시작 측점 → 마우스 측점 · 고른 부분 구간은 고정 띠.
|
||||
const bandView = createRangeBandView((hoverM) => {
|
||||
const start = tools.pendingStation();
|
||||
const sub = tools.subRun();
|
||||
return start !== null ? [start, hoverM] : sub ? [sub.fromM, sub.toM] : null;
|
||||
});
|
||||
const mountViews = (mode: string): void => {
|
||||
// [쉬프트] 붉은 강조는 그래프를 다시 그릴 때만 지워짐 — 다른 도구로 넘어가면 바로 뗌(271-1)
|
||||
if (mode !== "shift") lastGeom?.chart.querySelector(".b05-profile-runmark")?.remove();
|
||||
freeView.mount(lastGeom, mode === "free");
|
||||
bandView.mount(lastGeom, mode === "straighten");
|
||||
};
|
||||
|
||||
const tools = createProfileTools({
|
||||
onStraighten: (fromM, toM) => {
|
||||
const base = ctx.base();
|
||||
if (!base) return;
|
||||
ctx.applyEdits(straightenBetween(base, ctx.edits(), fromM, toM));
|
||||
// 이미 직선인 범위 = 부분 구간으로 고름(271-1) — ▲▼ 로 꺾으면 양 끝에 기본 L 곡선
|
||||
const alignment = ctx.alignment();
|
||||
const sub = alignment ? straightSubRun(alignment, fromM, toM) : null;
|
||||
if (sub) {
|
||||
tools.handleRunPick(sub);
|
||||
return;
|
||||
}
|
||||
ctx.applyEdits(straightenBetween(base, ctx.edits(), fromM, toM, ctx.savedRadii()));
|
||||
tools.clear();
|
||||
},
|
||||
onShift: (runs, delta) => {
|
||||
@@ -159,12 +181,12 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
const alignment = ctx.alignment();
|
||||
return alignment ? hasStraightRuns(alignment) : false;
|
||||
},
|
||||
onChanged: ctx.refresh,
|
||||
onModeChange: (mode) => freeView.mount(lastGeom, mode === "free"),
|
||||
onChanged: () => (bandView.redraw(), ctx.refresh()),
|
||||
onModeChange: mountViews,
|
||||
freeHint: () => free.hint(),
|
||||
mountOverlay: (geom) => {
|
||||
lastGeom = geom;
|
||||
freeView.mount(geom, tools.mode() === "free");
|
||||
mountViews(tools.mode());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -178,15 +200,10 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
if (mode === "shift") {
|
||||
return tools.handleRunPick(alignment ? detectStraightRun(alignment, chainageM) : null);
|
||||
}
|
||||
// 직선화 모드에서 첫 클릭이 **이미 직선화된 라인 안쪽**이면 그 라인을 골라
|
||||
// 틸팅(가운데 라운드 + 양측 탄젠트) 대상으로 삼는다.
|
||||
if (tools.pendingStation() === null && alignment) {
|
||||
const run = detectStraightRun(alignment, chainageM);
|
||||
if (run && Math.abs(run.fromM - chainageM) > 1e-6 && Math.abs(run.toM - chainageM) > 1e-6) {
|
||||
return tools.handleRunPick(run);
|
||||
}
|
||||
}
|
||||
return tools.handleStationPick(chainageM);
|
||||
// 직선화 클릭 = 늘 측점 고름 · 빈 곳은 가장 가까운 측점선에 붙음(271-1) — 직선 안 클릭이
|
||||
// 직선 통째 고름(붉은 강조)으로 새던 갈림을 지움. 이미 직선인 범위는 `onStraighten` 이 가름.
|
||||
const station = lastGeom ? snapStation(lastGeom.stations, chainageM) : null;
|
||||
return tools.handleStationPick(station ?? chainageM);
|
||||
}
|
||||
|
||||
function selectedIrregular(): IrregularStation | null {
|
||||
@@ -355,10 +372,33 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
|
||||
/* Esc = 도구 몫(271-2 · 3) — 잡는 단계에서 먼저 받아 고름 풀기(`design_pick_escape`)로 안 샘:
|
||||
* [직선화] 시작 측점 · 부분 구간만 취소(모드 유지) · [자유지정]은 자기 규칙(범위 첫 측점만 = 시작만). */
|
||||
const onEscape = (event: KeyboardEvent): void => {
|
||||
if (event.key !== "Escape" || !ctx.root.isConnected) return;
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (
|
||||
target?.closest?.("input, select, textarea") ||
|
||||
document.querySelector(".ui-modal__backdrop")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (tools.mode() === "free") {
|
||||
free.key("Escape");
|
||||
freeView.redraw();
|
||||
} else if (!(tools.mode() === "straighten" && tools.cancelPick())) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
window.addEventListener("keydown", onEscape, true);
|
||||
|
||||
const setListening = (on: boolean): void => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
if (on) window.addEventListener("keydown", onKeyDown);
|
||||
else stopHold();
|
||||
window.removeEventListener("keydown", onEscape, true);
|
||||
if (on) {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keydown", onEscape, true);
|
||||
} else stopHold();
|
||||
};
|
||||
return { tools, history, handleToolPick, setListening };
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
* [새 창] 과 같다(공용 토큰 `--btn-bar-*`). [새 창] 은 종단도가 그릴 때마다 맨 앞에 옮겨 붙인다(282-2).
|
||||
*
|
||||
* 선택 흐름:
|
||||
* [직선화] → 측점 2개를 그래프에서 고름 → 두 측점의 라운드에 탄젠트한 직선으로 대체.
|
||||
* [직선화] → 측점 2개를 그래프에서 고름(클릭 = 늘 측점 · 271-1) → 두 측점의 라운드에 탄젠트한 직선으로 대체.
|
||||
* 고른 범위가 이미 직선이면 그 범위를 부분 구간으로 고름 → [▲][▼]로 꺾으면 가운데에
|
||||
* 라운드 · 양 끝에 기본 L 곡선. Esc = 시작(또는 부분 구간)만 취소 · 모드 유지.
|
||||
* [쉬프트] → 직선화된 라인을 고름(여러 개 가능) → [▲][▼]로 통째로 올리고 내림.
|
||||
* 직선을 고른 채 [직선화] 모드에서 [▲][▼]를 누르면 그 직선이 꺾이며 가운데에
|
||||
* 라운드가 생기고 양쪽이 탄젠트 직선이 된다.
|
||||
* [자유지정] → 범위를 고르고 클릭으로 계획선을 잇는다(`design_profile_free` 몫 · 215-1).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -55,8 +55,12 @@ export interface ProfileTools {
|
||||
handleStationPick: (chainageM: number) => boolean;
|
||||
/** 그래프에서 직선을 눌렀을 때(구간 판정 결과) — 도구가 삼켰으면 true. */
|
||||
handleRunPick: (run: StraightRun | null) => boolean;
|
||||
/** 선택 중인 직선 구간들(강조 표시용). */
|
||||
/** [쉬프트]로 고른 직선 구간들(붉은 강조용) — [직선화] 부분 구간은 `subRun`(보라 띠 · 271-1). */
|
||||
selectedRuns: () => StraightRun[];
|
||||
/** [직선화] 중 고른 이미 직선인 부분 구간(▲▼ 꺾기 대상). */
|
||||
subRun: () => StraightRun | null;
|
||||
/** [직선화] 시작 측점 · 부분 구간만 취소(Esc · 모드 유지) — 취소한 것이 있으면 true. */
|
||||
cancelPick: () => boolean;
|
||||
/** 직선화 대기 중 첫 측점(강조 표시용). */
|
||||
pendingStation: () => number | null;
|
||||
/** 고른 직선을 ▲▼ 버튼과 같은 경로로 옮긴다(방향키가 같이 쓴다) — 대상이 없으면 false. */
|
||||
@@ -117,7 +121,15 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
return {
|
||||
mode: () => mode,
|
||||
nudge: step,
|
||||
selectedRuns: () => runs,
|
||||
selectedRuns: () => (mode === "shift" ? runs : []),
|
||||
subRun: () => (mode === "straighten" ? (runs[0] ?? null) : null),
|
||||
cancelPick() {
|
||||
if (mode !== "straighten" || (pending === null && !runs.length)) return false;
|
||||
pending = null;
|
||||
runs = [];
|
||||
callbacks.onChanged();
|
||||
return true;
|
||||
},
|
||||
pendingStation: () => pending,
|
||||
mountOverlay: callbacks.mountOverlay,
|
||||
clear() {
|
||||
@@ -129,6 +141,7 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
if (mode !== "straighten") return false;
|
||||
if (pending === null) {
|
||||
pending = chainageM;
|
||||
runs = [];
|
||||
callbacks.onChanged();
|
||||
return true;
|
||||
}
|
||||
@@ -222,7 +235,12 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
} else if (mode === "straighten" && pending !== null) {
|
||||
const hint = document.createElement("em");
|
||||
hint.className = "b05-route-profile__tool-hint";
|
||||
hint.textContent = `${pending.toFixed(1)}m 선택 — 두 번째 측점을 고르세요 (취소는 [직선화] 다시 누르기)`;
|
||||
hint.textContent = `${pending.toFixed(1)}m 선택 — 두 번째 측점을 고르세요 (Esc 시작 취소)`;
|
||||
wrap.append(hint);
|
||||
} else if (mode === "straighten" && runs.length === 1) {
|
||||
const hint = document.createElement("em");
|
||||
hint.className = "b05-route-profile__tool-hint";
|
||||
hint.textContent = "이미 직선 — ▲▼ 로 꺾으면 양 끝에 기본 L 곡선";
|
||||
wrap.append(hint);
|
||||
} else if (mode === "shift" && !runs.length) {
|
||||
const hint = document.createElement("em");
|
||||
|
||||
@@ -358,6 +358,15 @@
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
/* 범위 「시작–끝 · 길이」 글 — [직선화] · [자유지정] 실시간 띠 위(271-2 · 3). */
|
||||
.b05-profile-freemark__span {
|
||||
fill: var(--color-primary);
|
||||
font-size: var(--text-caption);
|
||||
paint-order: stroke;
|
||||
stroke: var(--color-surface);
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
/* 지나온 선 · 미리 보기 · 점 = 녹색(367-4) — 범위 띠 · 경계는 보라 그대로. */
|
||||
.b05-profile-freemark__line {
|
||||
stroke: var(--color-success);
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
* `resources/tester/test_346_2_pipeline_cross_page.py`.
|
||||
* crossWithUphill cross 단계 입력에 단면 유형 기본값(상단측)을 얹음 — 서버 노선 저장이 단면 유형 ·
|
||||
* 측구 방향을 새 지형으로 정하는 것과 같은 규칙(넘기는 칸 = 부르는 쪽 입력).
|
||||
* draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 얹힌 시설 제원).
|
||||
* crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457).
|
||||
* draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 그림 자료 · 카드 이름 = 지금
|
||||
* 현황을 새 측점에 다시 얹음 · 캐시 `withDraftShapes` 와 같은 `attachShapes` · 457).
|
||||
*
|
||||
* 순수 — 화면 · fetch · 세션 없음.
|
||||
* ========================================================================== */
|
||||
@@ -26,24 +28,26 @@ import type {
|
||||
import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { toDesignProfile } from "./B05_Profile_UI_Profile_Data";
|
||||
import { stationLabel } from "../../common_util/common_util_station";
|
||||
import { attachShapes, type AttachType } from "../../common_util/common_util_structure_attach";
|
||||
import { registryDefaults } from "../../common_util/common_util_culvert_sets";
|
||||
import {
|
||||
planStations,
|
||||
SAME_STATION_M,
|
||||
type PlanPipe,
|
||||
type PlanStructure,
|
||||
type PlanType,
|
||||
} from "../../common_util/common_util_structure_station";
|
||||
import { stationIdOf } from "./B05_Profile_Pipeline_Grade";
|
||||
|
||||
/** 측점에 얹힌 시설 제원(저장 상세에서 측점 열쇠로 옮김) — 낡음 판정 · 그림이 읽음 · 카드 번호(`station_id` ·
|
||||
* 있으면 저장 카드 번호를 이어 씀 · 440). */
|
||||
export type SectionSpecs = Partial<
|
||||
Pick<
|
||||
CrossSection,
|
||||
| "station_id"
|
||||
| "structure"
|
||||
| "culvert"
|
||||
| "ford"
|
||||
| "box"
|
||||
| "ford_pavement"
|
||||
| "revetment"
|
||||
| "pavement"
|
||||
| "underdrain"
|
||||
>
|
||||
>;
|
||||
/** 측점 열쇠로 이어 받는 카드 번호(`station_id` · 저장 카드 번호를 이어 씀 · 440) — 그림 자료 · 이름은 안 옮김(457). */
|
||||
export type SectionSpecs = Partial<Pick<CrossSection, "station_id">>;
|
||||
|
||||
/** 지금 현황(follow 뒤 관 · 구조물) + 구조물 표 — 새 측점에 그림 · 이름 · 시설 맥락을 다시 얹는 재료(457). */
|
||||
export interface ShapeStatus {
|
||||
pipes: ReadonlyArray<PlanPipe>;
|
||||
structures: ReadonlyArray<PlanStructure>;
|
||||
types: ReadonlyArray<AttachType>;
|
||||
}
|
||||
|
||||
/** cross 단계 입력 꼴(`B06_Section_Cross_Stage` `CrossStageInput`) — 칸은 그 단계 몫. */
|
||||
export interface CrossInput {
|
||||
@@ -128,19 +132,6 @@ const kindOf = (station: PipelineStation): SectionStation["kind"] =>
|
||||
? "curve"
|
||||
: "irregular";
|
||||
|
||||
/** 제원의 누가거리 = 새 측점(서버 부착은 따라간 관 · 구조물 자리 = 그 측점) — 옛 값이면 구조물 면적이 관 자리를 잘못 잡음(349-0n). */
|
||||
function atChainage(own: SectionSpecs, chainageM: number): SectionSpecs {
|
||||
const out: Record<string, unknown> = { ...own };
|
||||
for (const [name, spec] of Object.entries(own))
|
||||
if (
|
||||
spec &&
|
||||
typeof spec === "object" &&
|
||||
typeof (spec as { chainage_m?: unknown }).chainage_m === "number"
|
||||
)
|
||||
out[name] = { ...spec, chainage_m: chainageM };
|
||||
return out as SectionSpecs;
|
||||
}
|
||||
|
||||
/** 저장 상세 → 묶음 cross 출력(365-4 · 저장 노선 = 다시 셈 창구 한 벌이 고친 설계 그대로) — 측점이 상세와
|
||||
* 다르면(갈아끼움 전) null. */
|
||||
export function detailCross(
|
||||
@@ -181,7 +172,6 @@ export function stationSection(
|
||||
chainage_m: station.chainageM,
|
||||
label: stationLabel(station.chainageM, intervalM),
|
||||
kind: kindOf(station),
|
||||
...(own.structure ? { structure: own.structure } : {}),
|
||||
center_z: center,
|
||||
azimuth_deg: station.azimuthDeg,
|
||||
plan_radius_m: station.planRadiusM,
|
||||
@@ -215,8 +205,82 @@ export function stationSection(
|
||||
};
|
||||
}
|
||||
|
||||
/** 측점마다 그림 자료 · 카드 이름 = 지금 현황을 그 자리에 다시 얹음(캐시 `withDraftShapes` · 이름 = 측점 계획
|
||||
* 제목 · 457) — 옛 측점 그림을 옮기지 않음. */
|
||||
function attachStatus(sections: CrossSection[], status: ShapeStatus): void {
|
||||
const { pipes, structures, types } = status;
|
||||
attachShapes(sections as unknown as Array<Record<string, unknown>>, pipes, structures, types);
|
||||
const planned = planStations(pipes, structures, types, registryDefaults(types));
|
||||
for (const section of sections) {
|
||||
const hit = planned.find((p) => Math.abs(p.chainage_m - section.chainage_m) < SAME_STATION_M);
|
||||
if (hit) section.structure = hit.title;
|
||||
else delete section.structure;
|
||||
}
|
||||
}
|
||||
|
||||
/** 현황 묶음(입력 칸은 묶음 모양 `unknown` · 457) — 정의 옵션 · 이름이 빈 종류는 빈 목록 · 종류 id. */
|
||||
export const shapeStatus = (
|
||||
pipes: unknown,
|
||||
structures: unknown,
|
||||
types: ReadonlyArray<PlanType & { name?: string }>,
|
||||
): ShapeStatus =>
|
||||
({
|
||||
pipes,
|
||||
structures,
|
||||
types: types.map((type) => ({
|
||||
...type,
|
||||
name: type.name ?? type.type_id,
|
||||
options: type.options ?? [],
|
||||
})),
|
||||
}) as ShapeStatus;
|
||||
|
||||
/** 그 측점 그림 자료 → 얹힌 시설 맥락(B06 `Cross_Refresh` 와 같은 규칙 · 서버 `tilt_ranges` · `ford_drop_at` 짝). */
|
||||
function shapeContext(section: CrossSection): Record<string, unknown> {
|
||||
const drop = Number(section.ford?.overflow_depth_m);
|
||||
return {
|
||||
paved: Boolean(section.pavement?.length) || Boolean(section.ford_pavement),
|
||||
tilted: Boolean(section.ford_pavement || section.underdrain),
|
||||
tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3)
|
||||
surfaceDropM: drop > 0 ? drop : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** 측점마다 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457). */
|
||||
export function crossWithShapes(
|
||||
input: CrossInput | null,
|
||||
stations: ReadonlyArray<PipelineStation>,
|
||||
status: ShapeStatus | null,
|
||||
): CrossInput | null {
|
||||
if (!input || !status) return input;
|
||||
const sections = stations.map((station) => ({ chainage_m: station.chainageM }) as CrossSection);
|
||||
attachShapes(
|
||||
sections as unknown as Array<Record<string, unknown>>,
|
||||
status.pipes,
|
||||
status.structures,
|
||||
status.types,
|
||||
);
|
||||
const byKey: CrossInput["byKey"] = { ...input.byKey };
|
||||
stations.forEach((station, i) => {
|
||||
byKey[station.key] = { ...byKey[station.key], ...shapeContext(sections[i]) };
|
||||
});
|
||||
return { ...input, byKey };
|
||||
}
|
||||
|
||||
/** cross 단계 입력 = 시설 맥락(새 자리 그림) · 단면 유형 기본값(상단측) · 소단(지금 구조물) 얹음 — 페이지 묶음 cross. */
|
||||
export const crossWithContext = (
|
||||
input: CrossInput | null,
|
||||
stations: ReadonlyArray<PipelineStation>,
|
||||
status: ShapeStatus | null,
|
||||
bermAt: BermAt | null,
|
||||
): CrossInput | null =>
|
||||
crossWithBerm(
|
||||
crossWithUphill(crossWithShapes(input, stations, status), stations),
|
||||
stations,
|
||||
bermAt,
|
||||
);
|
||||
|
||||
/** 묶음 출력 → 상세. 저장 상세의 종단 머리(이름)는 그대로 · 측점 · 지반 · 횡단 · 계획선 표본은 새 노선
|
||||
* (십자선 대체 높이도 초안 종단 · 413). */
|
||||
* (십자선 대체 높이도 초안 종단 · 413) · 그림 자료 · 카드 이름 = `status`(지금 현황)를 새 측점에 다시 얹음(457). */
|
||||
export function draftSectionDetail(
|
||||
saved: SectionDetailResponse,
|
||||
ground: GroundOutput,
|
||||
@@ -224,15 +288,12 @@ export function draftSectionDetail(
|
||||
cross: CrossOutput | null,
|
||||
specs: ReadonlyMap<string, SectionSpecs>,
|
||||
intervalM: number,
|
||||
status: ShapeStatus | null = null,
|
||||
): SectionDetailResponse {
|
||||
const sections: CrossSection[] = ground.stations.map((station) =>
|
||||
stationSection(
|
||||
station,
|
||||
cross,
|
||||
atChainage(specs.get(station.key) ?? {}, station.chainageM),
|
||||
intervalM,
|
||||
),
|
||||
stationSection(station, cross, specs.get(station.key) ?? {}, intervalM),
|
||||
);
|
||||
if (status) attachStatus(sections, status);
|
||||
const { chainageM, z } = ground.longitudinal;
|
||||
return {
|
||||
longitudinal: {
|
||||
|
||||
@@ -18,6 +18,8 @@ import type { AlignmentCurve, AlignmentPolicy } from "./B05_Profile_UI_Profile_A
|
||||
export const DEFAULT_CURVE_LENGTH_M = 20;
|
||||
/** 초기값 직선 문턱 R(m) — config `initial_straight_radius_m` 짝. */
|
||||
export const INITIAL_STRAIGHT_RADIUS_M = 500;
|
||||
/** 법정 미달 판정 오차(m) — R = L/Δ 를 반올림 높이로 되셈한 L 19.99998 거짓 미달 막음(271-1) · 파이썬 짝. */
|
||||
export const SHORTFALL_TOLERANCE_M = 1e-3;
|
||||
|
||||
/** 안쪽 꼭짓점 중 기본 L 기준 R > 500 m(|대수차| < L/500) 인 것 — 파이썬 `flat_bends` · 시종점 false. */
|
||||
export function flatBends(
|
||||
@@ -72,7 +74,10 @@ export function verticalCurveShortfalls(
|
||||
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)
|
||||
.filter(
|
||||
(curve) =>
|
||||
curve.r_m < minR - SHORTFALL_TOLERANCE_M || curve.l_m < minL - SHORTFALL_TOLERANCE_M,
|
||||
)
|
||||
.map(
|
||||
(curve) =>
|
||||
`${curve.chainage_m.toFixed(1)}m: 종단곡선 R ${curve.r_m.toFixed(0)}m · L ${curve.l_m.toFixed(1)}m — ` +
|
||||
|
||||
@@ -109,6 +109,24 @@ export function hasStraightRuns(alignment: ProfileAlignment): boolean {
|
||||
return alignment.pvi.some((_node, index) => straightAt(alignment.pvi, index));
|
||||
}
|
||||
|
||||
/**
|
||||
* 고른 범위가 이미 한 직선이면 그 범위를 부분 구간으로 돌려준다(271-1) — [직선화] ▲▼ 로 꺾으면
|
||||
* 양 끝에 기본 L 곡선. 사이 꼭짓점이 없거나 하나라도 꺾였으면 null(보통 직선화).
|
||||
*/
|
||||
export function straightSubRun(
|
||||
alignment: ProfileAlignment,
|
||||
fromChainageM: number,
|
||||
toChainageM: number,
|
||||
): StraightRun | null {
|
||||
const fromM = Math.min(fromChainageM, toChainageM);
|
||||
const toM = Math.max(fromChainageM, toChainageM);
|
||||
const inside = alignment.pvi
|
||||
.map((node, index) => ({ at: node.chainage_m, index }))
|
||||
.filter(({ at }) => at > fromM + SAME_STATION_M && at < toM - SAME_STATION_M);
|
||||
if (!inside.length || !inside.every(({ index }) => straightAt(alignment.pvi, index))) return null;
|
||||
return { fromM, toM, nodes: [fromM, ...inside.map(({ at }) => at), toM] };
|
||||
}
|
||||
|
||||
/** 두 측점 사이(끝 제외)의 변화점 chainage. */
|
||||
function interiorNodes(alignment: ProfileAlignment, fromM: number, toM: number): number[] {
|
||||
return pviChainages(alignment).filter(
|
||||
@@ -131,17 +149,33 @@ function withOffsets(
|
||||
return { station_offsets: stationOffsets, curve_radii: curveRadii };
|
||||
}
|
||||
|
||||
/**
|
||||
* 범위 끝 R 이 저장 자동 R 과 같으면 지운다 — 끝 곡선 = 기본 L(271-1 · ▲▼ 433-13 과 같은 규칙) ·
|
||||
* 사용자 R 은 둠. `radii` 를 고쳐 쓴다.
|
||||
*/
|
||||
export function dropAutoEndRadii(
|
||||
radii: Record<string, number>,
|
||||
ends: number[],
|
||||
autoRadii: Readonly<Record<string, number>>,
|
||||
): void {
|
||||
ends.map(chainageKey).forEach((key) => {
|
||||
if (radii[key] !== undefined && radii[key] === autoRadii[key]) delete radii[key];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 측점을 직선으로 잇는다 — 사이 측점을 그 직선 위로 옮기고 라운드를 지운다.
|
||||
*
|
||||
* 양 끝 측점의 표고는 건드리지 않는다. 그 자리에 라운드가 있으면 새 직선이 그 라운드의
|
||||
* 접선이 된다(변화점 z는 그대로이고 좌우 직선만 바뀌므로 자동으로 성립한다).
|
||||
* 끝 R 이 저장 자동 R(`autoRadii`)이면 지워 기본 L(271-1).
|
||||
*/
|
||||
export function straightenBetween(
|
||||
base: AlignmentBase,
|
||||
edits: AlignmentEdits,
|
||||
fromChainageM: number,
|
||||
toChainageM: number,
|
||||
autoRadii: Readonly<Record<string, number>> = {},
|
||||
): AlignmentEdits {
|
||||
const fromM = Math.min(fromChainageM, toChainageM);
|
||||
const toM = Math.max(fromChainageM, toChainageM);
|
||||
@@ -156,11 +190,13 @@ export function straightenBetween(
|
||||
const target = startZ + ((endZ - startZ) * (chainage - fromM)) / span;
|
||||
return [chainageKey(chainage), target - baseElevationAt(base, chainage)];
|
||||
});
|
||||
return withOffsets(
|
||||
const next = withOffsets(
|
||||
edits,
|
||||
[...updates, ...pinEnds(base, current, [fromM, toM])],
|
||||
inner.map(chainageKey),
|
||||
);
|
||||
dropAutoEndRadii(next.curve_radii, [fromM, toM], autoRadii);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,6 +281,8 @@ export function tiltStraightRun(
|
||||
if (chainage < pivot) place(chainage, run.fromM, startZ, pivot, pivotZ);
|
||||
else place(chainage, pivot, pivotZ, run.toM, endZ);
|
||||
});
|
||||
// 부분 구간(271-1) 끝이 꼭짓점 아닌 측점이면 지금 높이로 세움 — 끝이 따라 움직이지 않게
|
||||
updates.push(...pinEnds(base, current, [run.fromM, run.toM]));
|
||||
|
||||
const gradeIn = (pivotZ - startZ) / Math.max(pivot - run.fromM, 1e-12);
|
||||
const gradeOut = (endZ - pivotZ) / Math.max(run.toM - pivot, 1e-12);
|
||||
|
||||
Reference in New Issue
Block a user