Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1
# Conflicts: # B06_Section/B06_Section_Api_Types.ts
This commit is contained in:
@@ -109,6 +109,10 @@ export const STATE_REGISTRY = {
|
||||
culvertmove: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertmove:${p}:${r}` },
|
||||
/** 암 경계선 오프셋(측점별). */
|
||||
rockb: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:rockb:${p}:${r}` },
|
||||
/** 측점별 암 절토 경사비(1:n 의 n) — 카드에서 넣은 사용자 값(2026-09-07).
|
||||
* 암 경계선(`rockb`)과 같은 자리·같은 꼴이다. 재계산에 **계산 전에** 실어 보내야
|
||||
* 설계선이 새 경사로 그려진다(값만 베껴 붙이면 그림과 숫자가 어긋난다). */
|
||||
cutslope: { bucket: "draft", scope: "route" },
|
||||
/** 소단 제원(측점키 → {width_m, interval_m, slope_deg}) — 계획서 3-9.
|
||||
* 사용자가 구간에 놓은 값이라 재계산에 함께 실어 보내야 한다. 안 실으면 계획선을
|
||||
* 고치는 순간 계단이 사라진다(암 경계선이 옛 키를 보던 것과 같은 자리). */
|
||||
|
||||
@@ -255,6 +255,8 @@ export async function previewCrossDesigns(
|
||||
rockBoundaryOffsets?: Record<string, number>;
|
||||
/** 측점별 소단 제원(chainage 키 → 폭·간격·기울기). 값이 없는 측점은 소단 없음. */
|
||||
berms?: Record<string, { width_m: number; interval_m: number; slope_deg: number }>;
|
||||
/** 측점별 암 절토 경사비(chainage 키 → 1:n 의 n). 0 은 「표준값을 씀」이다. */
|
||||
cutSlopeRatios?: Record<string, number>;
|
||||
},
|
||||
): Promise<CrossDesignPreviewResponse> {
|
||||
return requestJson<CrossDesignPreviewResponse>(
|
||||
@@ -267,6 +269,7 @@ export async function previewCrossDesigns(
|
||||
full_designs: options?.fullDesigns ?? false,
|
||||
rock_boundary_offsets: options?.rockBoundaryOffsets ?? null,
|
||||
berms: options?.berms ?? null,
|
||||
cut_slope_ratios: options?.cutSlopeRatios ?? null,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -464,6 +464,9 @@ export interface CrossDesign {
|
||||
/** 이 측점에 놓인 소단 제원(계획서 3-9). 없으면 계단 없음.
|
||||
* 세션이 비어도(확정 뒤·다른 PC) 저장분만으로 계단이 서게 되싣는 값이다. */
|
||||
berm?: { width_m: number; interval_m: number; slope_deg: number };
|
||||
/** 측점별 **암 절토 경사비**(1:n 의 n) — 사용자가 카드에 넣은 값(2026-09-07).
|
||||
* 0 은 「표준값을 씀」이다. 계산에 쓰이는 값이 아니라 **입력을 되싣는 자리**다. */
|
||||
cut_slope_ratio_user?: number;
|
||||
}
|
||||
|
||||
export interface CrossDesignResponse {
|
||||
@@ -485,6 +488,8 @@ export interface CrossDesignRequest {
|
||||
rock_boundary_offset_m?: number | null;
|
||||
/** 암 지반 2단계 경사 적용 여부(기본 true, 토글로 해제). */
|
||||
two_stage_slope?: boolean;
|
||||
/** 이 측점만 쓰는 암 절토 경사비(1:n 의 n). 없으면 표준값(2026-09-07). */
|
||||
cut_slope_ratio?: number | null;
|
||||
/** 측구 생성 여부. null/미지정=자동 판정, true/false=수동 override. */
|
||||
ditch_enabled?: boolean | null;
|
||||
/** 설정 패널 편집값. 요청값 → config 기본값 순으로 우선한다. */
|
||||
@@ -497,6 +502,8 @@ export interface CrossSectionPatch {
|
||||
rock_boundary_offset_m?: number;
|
||||
/** 측점 개별 표시 반폭(m, 2026-08-06 사용자 지시). */
|
||||
display_half_width_m?: number;
|
||||
/** 측점별 암 절토 경사비(2026-09-07). 0 = 표준값으로 되돌림. */
|
||||
cut_slope_ratio_user?: number;
|
||||
inlet_structure?: "auto" | "revet" | "I" | "L" | "U";
|
||||
basin_adjust?: {
|
||||
innerWidthM: number;
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
toAlignmentBase,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||
import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
import { readState } from "../A00_Common/b_page_state";
|
||||
|
||||
/** 계획선 편집 델타 — B05 `AlignmentEdits`와 저장분 `profile_alignment.edits`가 같은 모양이다. */
|
||||
export interface CrossRefreshEdits {
|
||||
@@ -83,6 +84,8 @@ export const USER_TOUCHED_KEYS = [
|
||||
"revet_follow_grade",
|
||||
// 소단 제원 — 사용자가 구간에 놓은 값이라 다시 계산해도 살려 둔다(계획서 3-9).
|
||||
"berm",
|
||||
// 측점별 암 절토 경사(2026-09-07) — 표준을 바꿔도 개별로 고친 측점은 그대로 둔다.
|
||||
"cut_slope_ratio_user",
|
||||
] as const;
|
||||
|
||||
/** 다시 계산해도 살려 두는 값 — 위 사용자 값에 **상태를 나르는 둘**을 더한 것. */
|
||||
@@ -154,6 +157,23 @@ function bermPayload(
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 측점별 암 절토 경사비 세션값 — 암 경계선(`readRockOffsets`)과 같은 방식으로 읽는다.
|
||||
*
|
||||
* **0 은 「표준값을 씀」**이다(되돌리기). 그래서 0 을 거르지 않고 그대로 싣는다 — 거르면
|
||||
* 저장분에 남은 옛 사용자 값이 되살아난다.
|
||||
*/
|
||||
function readCutSlopeRatios(projectId: string, routeId: number): Map<number, number> {
|
||||
const out = new Map<number, number>();
|
||||
const raw = readState<Record<string, number>>("cutslope", projectId, routeId);
|
||||
if (!raw) return out;
|
||||
for (const [chainage, ratio] of Object.entries(raw)) {
|
||||
const at = Number(chainage);
|
||||
if (Number.isFinite(at) && Number.isFinite(ratio) && ratio >= 0) out.set(rockKey(at), ratio);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 브라우저 안에서 전 측점을 다시 계산한다(정상 경로).
|
||||
*
|
||||
@@ -184,6 +204,9 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
|
||||
const rockOffsets = readRockOffsets(projectId, input.routeId);
|
||||
const rockDefault = readRockBoundaryDefault(projectId);
|
||||
// 측점별 암 절토 경사도 세션에만 있는 값이다 — **계산 전에** 실어야 설계선이 새 경사로
|
||||
// 그려진다. 계산 뒤에 값만 베껴 붙이면 그림은 옛 경사, 숫자만 새것이 된다(2026-09-07).
|
||||
const cutSlopeRatios = readCutSlopeRatios(projectId, input.routeId);
|
||||
// 소단도 같은 성격 — 세션에만 있는 값이라 여기서 실어 주지 않으면 계획선을 고치는
|
||||
// 순간 계단이 사라진다(계획서 3-9).
|
||||
const bermSpans = readBermSpans(projectId, input.routeId);
|
||||
@@ -195,6 +218,13 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
};
|
||||
// 카드 버튼 선택은 세션 초안이 정본보다 새것이다 — 새로고침 뒤에도 고른 값이 남는다
|
||||
// (2026-09-06 사용자 확정: 조작은 캐시, 저장은 [저장]·[확정]).
|
||||
/** 세션 → 저장분 순. 없거나 0(되돌림)이면 null 을 줘 표준값을 쓰게 한다. */
|
||||
const cutSlopeAt = (chainageM: number, design: Record<string, unknown>): number | null => {
|
||||
const session = cutSlopeRatios.get(rockKey(chainageM));
|
||||
if (session !== undefined) return session > 0 ? session : null;
|
||||
const stored = design.cut_slope_ratio_user;
|
||||
return typeof stored === "number" && stored > 0 ? stored : null;
|
||||
};
|
||||
const choices = crossDesignChoices(projectId, input.routeId);
|
||||
const choiceAt = (chainageM: number) => choices.get(Math.round(chainageM * 100) / 100);
|
||||
|
||||
@@ -237,6 +267,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
standard,
|
||||
berm: bermAt(section.chainage_m),
|
||||
rockBoundaryOffsetM,
|
||||
cutSlopeRatio: cutSlopeAt(section.chainage_m, design),
|
||||
twoStageSlope:
|
||||
choice?.two_stage_slope ??
|
||||
(design.two_stage_slope === undefined ? true : Boolean(design.two_stage_slope)),
|
||||
@@ -276,6 +307,10 @@ async function refreshFromServer(input: CrossRefreshInput): Promise<number[]> {
|
||||
fullDesigns: true,
|
||||
rockBoundaryOffsets: readRockBoundarySession(projectId, routeId),
|
||||
berms: bermPayload(projectId, routeId, detail),
|
||||
// 측점별 암 절토 경사도 세션값이라 함께 싣는다 — 안 실으면 서버 폴백에서만
|
||||
// 사용자 경사가 조용히 표준값으로 되돌아간다(2026-09-07).
|
||||
cutSlopeRatios:
|
||||
readState<Record<string, number>>("cutslope", projectId, routeId) ?? undefined,
|
||||
},
|
||||
);
|
||||
if (shouldApply && !shouldApply()) return [];
|
||||
|
||||
@@ -568,6 +568,7 @@ def compute_cross_design(
|
||||
standard: dict[str, Any] | None = None,
|
||||
rock_boundary_offset_m: float | None = None,
|
||||
two_stage_slope: bool = True,
|
||||
cut_slope_ratio: float | None = None,
|
||||
ditch_enabled: bool | None = None,
|
||||
surface_drop_m: float = 0.0,
|
||||
plan_radius_m: float | None = None,
|
||||
@@ -584,6 +585,8 @@ def compute_cross_design(
|
||||
standard: B06 설정 패널 편집값(STANDARD_CROSS_SECTION 형태). 요청값 → config 순.
|
||||
rock_boundary_offset_m: 암반 경계선 오프셋(지반선 기준, 음수=하향). 암 지반 2단계 절토용.
|
||||
two_stage_slope: 암 지반에서 암반 경계 기준 2단계 경사 적용 여부(기본 True, 토글로 해제).
|
||||
cut_slope_ratio: 이 측점만 쓰는 절토 경사비(1:n 의 n). 사용자가 카드에서 넣은 값이며
|
||||
None 이면 표준 횡단면 설정값을 그대로 쓴다(2026-09-07 사용자 지시).
|
||||
plan_radius_m: 이 측점의 평면 곡선반경(m). 곡선부 확폭(별표2 Ⅰ.2.나.(4))을 정하는
|
||||
입력이며, None·45m 이상이면 확폭이 없다.
|
||||
curve_outer_side: 곡선 **바깥쪽**("left"/"right"). 확폭은 그쪽으로만 붙는다
|
||||
@@ -607,6 +610,11 @@ def compute_cross_design(
|
||||
if ditch_type == "l_type" and preset_key != "rock":
|
||||
raise ValueError("L형 측구는 암(리핑암/발파암) 구간에서만 선택할 수 있습니다.")
|
||||
group = _resolve_group(preset_key, standard)
|
||||
# 측점 하나만 다른 절토 경사(2026-09-07 사용자 지시) — 표준 그룹의 경사비만 갈아 끼운다.
|
||||
# ⚠ 여기서 갈아야 아래 기하·소단이 전부 새 경사를 따른다. 계산이 끝난 뒤 결과에 값만
|
||||
# 베껴 붙이면 설계선은 옛 경사로 그려지고 숫자만 새것이 되어 어긋난다.
|
||||
if cut_slope_ratio is not None and float(cut_slope_ratio) > 0:
|
||||
group = {**group, "cut_slope_ratio": float(cut_slope_ratio)}
|
||||
paved_group = _resolve_group("paved", standard)
|
||||
# 포장 중첩: 횡단경사와 포장층 두께만 포장 그룹을 따른다.
|
||||
cross_slope_pct = paved_group["cross_slope_pct"] if paved else group["cross_slope_pct"]
|
||||
|
||||
@@ -44,6 +44,7 @@ from B06_Section.B06_Section_Router_Design import (
|
||||
ford_drop_at,
|
||||
ford_surface_drops,
|
||||
stored_berm,
|
||||
stored_cut_slope,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
attach_default_designs as _attach_default_designs,
|
||||
@@ -555,6 +556,7 @@ async def preview_cross_designs(
|
||||
request.rock_boundary_offsets,
|
||||
project_root,
|
||||
request.berms,
|
||||
request.cut_slope_ratios,
|
||||
)
|
||||
|
||||
await asyncio.to_thread(rebuild)
|
||||
@@ -636,6 +638,8 @@ async def compute_cross_section_design(
|
||||
standard=request.standard_cross_section,
|
||||
rock_boundary_offset_m=request.rock_boundary_offset_m,
|
||||
two_stage_slope=request.two_stage_slope,
|
||||
# 측점별 암 절토 경사 — 요청값이 없으면 저장분에서 잇는다(2026-09-07).
|
||||
cut_slope_ratio=request.cut_slope_ratio or stored_cut_slope(stored_design or {}),
|
||||
ditch_enabled=request.ditch_enabled,
|
||||
surface_drop_m=ford_drop_at(request.chainage_m, ford_surface_drops(project_root)),
|
||||
berm=stored_berm(stored_design or {}),
|
||||
|
||||
@@ -140,6 +140,9 @@ USER_TOUCHED_KEYS = (
|
||||
"revet_follow_grade",
|
||||
# 소단 제원 — 사용자가 구간에 놓은 값이라 다시 계산해도 살려 둔다(계획서 3-9).
|
||||
"berm",
|
||||
# 측점별 암 절토 경사 — 표준 횡단면 설정을 바꿔도 개별로 고친 측점은 그대로 둔다
|
||||
# (2026-09-07 사용자 확정: 「사용자가 기본값을 사용하지 않는 값들은 변경되면 안됨」).
|
||||
"cut_slope_ratio_user",
|
||||
)
|
||||
|
||||
|
||||
@@ -193,6 +196,7 @@ def enforce_pavement_ranges(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
cut_slope_ratio=stored_cut_slope(design),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
||||
berm=stored_berm(design),
|
||||
@@ -245,6 +249,7 @@ def enforce_ford_surface_drops(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
cut_slope_ratio=stored_cut_slope(design),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
surface_drop_m=wanted,
|
||||
berm=stored_berm(design),
|
||||
@@ -346,6 +351,18 @@ def attach_default_designs(
|
||||
continue
|
||||
|
||||
|
||||
def stored_cut_slope(stored: dict[str, Any]) -> float | None:
|
||||
"""저장분에 남은 측점별 암 절토 경사비 — 없거나 0(표준값으로 되돌림)이면 None.
|
||||
|
||||
⚠ 소단과 같은 성격이다 — **경사는 나르는 값이 아니라 기하 입력**이라 계산에 넣어야 한다.
|
||||
다시 계산한 뒤 키만 베껴 붙이면 설계선은 옛 경사로 나오고 값만 새것이 되어 어긋난다.
|
||||
"""
|
||||
value = stored.get("cut_slope_ratio_user")
|
||||
if isinstance(value, (int, float)) and float(value) > 0:
|
||||
return float(value)
|
||||
return None
|
||||
|
||||
|
||||
def stored_berm(stored: dict[str, Any]) -> BermSpec | None:
|
||||
"""저장분에 남은 소단 제원 — 세션값이 없을 때 쓴다(확정 뒤·다른 PC)."""
|
||||
spec = stored.get("berm")
|
||||
@@ -369,6 +386,7 @@ def recompute_designs_for_alignment(
|
||||
rock_boundary_offsets: dict[str, float] | None = None,
|
||||
project_root: Path | None = None,
|
||||
berms: dict[str, dict[str, float]] | None = None,
|
||||
cut_slope_ratios: dict[str, float] | None = None,
|
||||
) -> None:
|
||||
modes = default_section_modes(longitudinal)
|
||||
pavement = pavement_suggestions(longitudinal)
|
||||
@@ -378,6 +396,13 @@ def recompute_designs_for_alignment(
|
||||
round(float(record["chainage_m"]), 3): (record.get("design") or {})
|
||||
for record in stored_designs
|
||||
}
|
||||
# 측점별 암 절토 경사(2026-09-07) — 0 은 「표준값을 씀」이라 저장분을 덮어 지운다.
|
||||
session_cut_slopes: dict[float, float] = {}
|
||||
for raw_key, ratio in (cut_slope_ratios or {}).items():
|
||||
try:
|
||||
session_cut_slopes[round(float(raw_key), 3)] = float(ratio)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
session_offsets: dict[float, float] = {}
|
||||
for raw_key, offset in (rock_boundary_offsets or {}).items():
|
||||
try:
|
||||
@@ -415,6 +440,11 @@ def recompute_designs_for_alignment(
|
||||
stored.get("rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M),
|
||||
),
|
||||
two_stage_slope=bool(stored.get("two_stage_slope", True)),
|
||||
cut_slope_ratio=(
|
||||
(session_cut_slopes[key] or None)
|
||||
if key in session_cut_slopes
|
||||
else stored_cut_slope(stored)
|
||||
),
|
||||
ditch_enabled=stored.get("ditch_enabled"),
|
||||
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
||||
berm=session_berms.get(key) or stored_berm(stored),
|
||||
|
||||
@@ -52,6 +52,9 @@ class CrossDesignRequest(BaseModel):
|
||||
rock_boundary_offset_m: float | None = None
|
||||
# 암 지반 2단계 경사(암반 경계 아래=암 경사, 위=토사 경사) 적용 여부. 토글로 해제 가능.
|
||||
two_stage_slope: bool = True
|
||||
# 이 측점만 쓰는 암 절토 경사비(1:n 의 n) — 카드에서 넣은 값(2026-09-07 사용자 지시).
|
||||
# None 이면 표준 횡단면 설정값을 쓴다.
|
||||
cut_slope_ratio: float | None = Field(default=None, gt=0)
|
||||
# 측구 생성 여부. None=자동 판정(측구측 절토면만 생성), True/False=수동 override.
|
||||
ditch_enabled: bool | None = None
|
||||
# B06 설정 패널 편집값(STANDARD_CROSS_SECTION 형태). 요청값 → config 기본값 순.
|
||||
@@ -136,6 +139,10 @@ class CrossSectionPatch(BaseModel):
|
||||
rock_boundary_offset_m: float | None = None
|
||||
# 측점별 표시 반폭(m) — 카드 개별 조절값. 전역 반폭과 다를 때만 실린다(2026-08-06).
|
||||
display_half_width_m: float | None = Field(default=None, gt=0)
|
||||
# 측점별 암 절토 경사비(1:n 의 n) — 카드에서 넣은 값(2026-09-07 사용자 지시).
|
||||
# **0 은 「표준값을 씀」**(되돌리기)이라 gt=0 이 아니라 ge=0 이다 — 0 이 와야
|
||||
# 정본에 남은 옛 사용자 값이 지워진다.
|
||||
cut_slope_ratio_user: float | None = Field(default=None, ge=0)
|
||||
inlet_structure: Literal["auto", "revet", "I", "L", "U"] | None = None
|
||||
basin_adjust: BasinAdjustPatch | None = None
|
||||
# 기슭막이 4축 조작값 — 키는 역할("inlet"/"outlet"/"extra0"…/"bextra0"…).
|
||||
@@ -302,6 +309,9 @@ class CrossDesignPreviewRequest(BaseModel):
|
||||
# 사용자가 구간에 놓은 소단을 확정 전에도 재계산에 반영한다(계획서 3-9).
|
||||
# 값이 없는 측점은 소단 없음 — 종전 설계 그대로다.
|
||||
berms: dict[str, dict[str, float]] | None = None
|
||||
# 측점별 암 절토 경사비(chainage 키 → 1:n 의 n). 위와 같은 성격 — 확정 전 세션값을
|
||||
# 재계산에 반영한다(2026-09-07). **0 은 「표준값을 씀」**(되돌리기)이다.
|
||||
cut_slope_ratios: dict[str, float] | None = None
|
||||
|
||||
def edits(self) -> dict[str, Any]:
|
||||
return {"station_offsets": self.station_offsets, "curve_radii": self.curve_radii}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design";
|
||||
import { buildCutSlopeControl, type CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
||||
import {
|
||||
buildDesignControls,
|
||||
buildRockBoundaryControl,
|
||||
@@ -122,11 +123,12 @@ export function appendCardHeader(
|
||||
card.append(header);
|
||||
}
|
||||
|
||||
/** 하단 정보행 — 중심고와 (암 지반일 때) 암 경계선 제어. */
|
||||
/** 하단 정보행 — 중심고와 (암 지반일 때) 암 경계선·암 절토 경사 제어. */
|
||||
export function appendCardFooter(
|
||||
card: HTMLElement,
|
||||
section: CrossSection,
|
||||
rockBoundary?: RockBoundaryControl,
|
||||
cutSlope?: CutSlopeControl,
|
||||
): void {
|
||||
const footer = document.createElement("footer");
|
||||
const center = document.createElement("span");
|
||||
@@ -138,5 +140,12 @@ export function appendCardFooter(
|
||||
rockControl.classList.add("b06-cross-card__rockb");
|
||||
footer.append(rockControl);
|
||||
}
|
||||
// 암 절토 경사(각도)는 암 측점에만 — 토사 측점은 암반이 없어 쓰이지 않는다
|
||||
// (2026-09-07 사용자 확정). 전체를 바꾸는 자리는 좌측 [표준 횡단면 설정]이다.
|
||||
if (cutSlope && section.design?.geometry_preset === "rock") {
|
||||
const slopeControl = buildCutSlopeControl(section, cutSlope);
|
||||
slopeControl.classList.add("b06-cross-card__cutslope");
|
||||
footer.append(slopeControl);
|
||||
}
|
||||
card.append(footer);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_CutSlope.ts
|
||||
* 측점 하나의 **암 절토 경사** 입력칸(2026-09-07 사용자 지시).
|
||||
*
|
||||
* 사용자 원문 — 「개별 횡단도에는 암 절토 각도의 개별 수정 가능해야함 / 전체 변경을 위해서는
|
||||
* 좌측 패널의 표준 횡단면 설정을 이용」. 그래서 **전체 기본값은 좌측 패널**, **이 칸은 그 측점
|
||||
* 하나**만 바꾼다. 표준을 바꿔도 여기서 만진 측점은 그대로다(사용자 값이라 재계산이 살려 둔다).
|
||||
*
|
||||
* 화면에는 **각도(°)**로 보이고 속으로는 경사비(1:n 의 n)로 다닌다 — 소단 폼의
|
||||
* 「안쪽 기울기 (°)」와 같은 말법이다. 1:0.4 = 68.2° · 1:1.0 = 45° · 1:1.5 = 33.7°.
|
||||
*
|
||||
* ⚠ 경사는 **나르는 값이 아니라 기하 입력**이다. 계산이 끝난 결과에 값만 베껴 붙이면 설계선은
|
||||
* 옛 경사로 그려지고 숫자만 새것이 된다 — 값은 반드시 **계산 전에** 넘어가야 한다
|
||||
* (`computeCrossDesign(cutSlopeRatio)` · `compute_cross_design(cut_slope_ratio=…)`).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
|
||||
/** 측점별 암 절토 경사 제어기 — Page 가 세션 저장소와 연결해 구현한다. */
|
||||
export interface CutSlopeControl {
|
||||
/** 세션 → design 저장값 → 표준값 순으로 지금 경사비를 돌려준다. */
|
||||
ratioFor: (section: CrossSection) => number;
|
||||
/** 이 측점의 표준값(되돌릴 자리) — 사용자가 안 만진 상태의 값. */
|
||||
standardRatioFor: (section: CrossSection) => number;
|
||||
/** 경사비를 넣는다. null 이면 표준값으로 되돌린다. */
|
||||
set: (chainageM: number, ratio: number | null) => void;
|
||||
}
|
||||
|
||||
/** 경사비(1:n 의 n) → 수평에서 잰 각도(°). n 이 작을수록 급하다. */
|
||||
export function ratioToDegrees(ratio: number): number {
|
||||
return (Math.atan(1 / Math.max(ratio, 1e-6)) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
/** 각도(°) → 경사비(1:n 의 n). 1~89° 밖은 사면이 서지 않아 잘라 받는다. */
|
||||
export function degreesToRatio(degrees: number): number {
|
||||
const clamped = Math.min(Math.max(degrees, 1), 89);
|
||||
return 1 / Math.tan((clamped * Math.PI) / 180);
|
||||
}
|
||||
|
||||
/** 0.1° 단위로 같은 값인가 — 표준값으로 되돌아왔는지 가리는 데 쓴다. */
|
||||
function sameDegrees(a: number, b: number): boolean {
|
||||
return Math.abs(a - b) < 0.05;
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 하단에 서는 「암 절토 68.2° ↺」 칸. 암 경계선 제어와 같은 자리·같은 모양이다.
|
||||
*
|
||||
* 되돌리기(↺)는 **표준 횡단면 설정값**으로 돌려놓는다 — 좌측 패널을 그 뒤에 바꾸면 이 측점도
|
||||
* 따라간다(사용자가 만진 흔적이 지워지므로).
|
||||
*/
|
||||
export function buildCutSlopeControl(section: CrossSection, control: CutSlopeControl): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b06-design__seg b06-design__cutslope";
|
||||
const legend = document.createElement("span");
|
||||
legend.className = "b06-design__seg-legend";
|
||||
legend.textContent = "암 절토";
|
||||
wrap.append(legend);
|
||||
|
||||
const group = document.createElement("div");
|
||||
group.className = "b06-design__seg-buttons";
|
||||
const ratio = control.ratioFor(section);
|
||||
const standard = control.standardRatioFor(section);
|
||||
const degrees = ratioToDegrees(ratio);
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.className = "b06-design__cutslope-input";
|
||||
input.step = "0.5";
|
||||
input.min = "1";
|
||||
input.max = "89";
|
||||
input.value = degrees.toFixed(1);
|
||||
input.title =
|
||||
`이 측점의 암 절토 경사 — 각도(°)로 넣는다.\n` +
|
||||
`지금 1:${ratio.toFixed(2)} (${degrees.toFixed(1)}°) · 표준 1:${standard.toFixed(2)} (${ratioToDegrees(standard).toFixed(1)}°)\n` +
|
||||
`전체를 바꾸려면 좌측 [표준 횡단면 설정]을 쓴다.`;
|
||||
const commit = (): void => {
|
||||
const entered = Number(input.value);
|
||||
if (!Number.isFinite(entered)) {
|
||||
input.value = degrees.toFixed(1);
|
||||
return;
|
||||
}
|
||||
// 표준값으로 되돌아온 입력은 사용자 값을 남기지 않는다 — 그래야 표준을 바꿀 때 따라간다.
|
||||
const next = sameDegrees(entered, ratioToDegrees(standard)) ? null : degreesToRatio(entered);
|
||||
control.set(section.chainage_m, next);
|
||||
};
|
||||
input.addEventListener("change", commit);
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
commit();
|
||||
}
|
||||
});
|
||||
// 카드 클릭(선택·줌)이 입력을 뺏지 않게 한다 — 암 경계선 버튼과 같은 태도.
|
||||
input.addEventListener("pointerdown", (event) => event.stopPropagation());
|
||||
|
||||
const unit = document.createElement("span");
|
||||
unit.className = "b06-design__cutslope-unit";
|
||||
unit.textContent = "°";
|
||||
|
||||
const reset = document.createElement("button");
|
||||
reset.type = "button";
|
||||
reset.className = "b06-design__rockb-btn is-reset";
|
||||
reset.textContent = "↺";
|
||||
reset.title = `표준값으로 되돌리기 (1:${standard.toFixed(2)} · ${ratioToDegrees(standard).toFixed(1)}°)`;
|
||||
reset.disabled = sameDegrees(degrees, ratioToDegrees(standard));
|
||||
reset.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
control.set(section.chainage_m, null);
|
||||
});
|
||||
|
||||
group.append(input, unit, reset);
|
||||
wrap.append(group);
|
||||
return wrap;
|
||||
}
|
||||
@@ -69,11 +69,19 @@ function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
const GROUND_OPTIONS: Array<[GroundType, keyof typeof ui_locales]> = [
|
||||
["soil", "B06_Design_Ground_Soil"],
|
||||
["ripping_rock", "B06_Design_Ground_Ripping"],
|
||||
["blasting_rock", "B06_Design_Ground_Blasting"],
|
||||
];
|
||||
/**
|
||||
* 지반유형은 **「토사」 토글 하나**다(2026-09-07 사용자 확정) — 켜면 토사, **끄면 암**(기본).
|
||||
*
|
||||
* 사용자 원문 — 「리핑암과 발파암 버튼을 삭제하면서 구분의 의미가 없어졌어. … 토사버튼만
|
||||
* 존재하고 이값은 활성화/비활성화로 반영(기본값은 비활성화)」. 까닭은 **암반 지정·범위가
|
||||
* 실무에서 애매해** 측점마다 암질을 못 박는 것이 오히려 틀리기 때문이고, 연암:경암 ·
|
||||
* 발파암:리핑암은 **설계내역에서 설계자가 비율로** 넣기로 했다(계획서 8-1).
|
||||
*
|
||||
* ⚠ 저장값은 종전 그대로 쓴다 — 끔 = `ripping_rock`, 켬 = `soil`. 옛 자료의 `blasting_rock`
|
||||
* 도 「암(끔)」으로 읽힌다. 유토곡선·수량은 암을 한 종류(리핑암)로 잡으며, 갈라 넣는 것은
|
||||
* 설계내역 몫이다.
|
||||
*/
|
||||
const GROUND_ROCK_DEFAULT: GroundType = "ripping_rock";
|
||||
const MODE_OPTIONS: Array<[SectionMode, keyof typeof ui_locales]> = [
|
||||
["left_cut", "B06_Design_Mode_LeftCut"],
|
||||
["right_cut", "B06_Design_Mode_RightCut"],
|
||||
@@ -321,7 +329,8 @@ export function buildDesignControls(
|
||||
twoStage: boolean;
|
||||
ditchEnabled: boolean | null;
|
||||
} = {
|
||||
ground: design?.ground_type ?? "soil",
|
||||
// 설계가 아직 없는 측점의 기본은 **암**이다(2026-09-07 사용자: 「기본값은 비활성화」).
|
||||
ground: design?.ground_type ?? GROUND_ROCK_DEFAULT,
|
||||
mode: design?.section_mode ?? (section.uphill_side === "right" ? "right_cut" : "left_cut"),
|
||||
ditch: design?.ditch_side ?? null,
|
||||
ditchType: design?.ditch_type ?? "standard",
|
||||
@@ -353,11 +362,24 @@ export function buildDesignControls(
|
||||
});
|
||||
};
|
||||
|
||||
// 지반유형: 제목행 배치용으로 분리 반환(D-6). 라벨 삭제(3번) — 버튼만.
|
||||
const groundSegment = segment("", GROUND_OPTIONS, state.ground, (value) => {
|
||||
state.ground = value;
|
||||
emit();
|
||||
});
|
||||
// 지반유형: 제목행 배치용으로 분리 반환(D-6). 「토사」 토글 하나 — 끄면 암이다(2026-09-07).
|
||||
// 버튼명은 「토사」로 고정하고 **켜짐/꺼짐(색)**으로 상태를 보인다 — 측구 토글과 같은 꼴
|
||||
// (사용자 원문: 「토사버튼만 존재하고 이값은 활성화/비활성화로 반영」).
|
||||
const groundToggle = toggle(
|
||||
"",
|
||||
state.ground === "soil",
|
||||
L("B06_Design_Ground_Soil"),
|
||||
L("B06_Design_Ground_Soil"),
|
||||
() => {
|
||||
state.ground = state.ground === "soil" ? GROUND_ROCK_DEFAULT : "soil";
|
||||
emit();
|
||||
},
|
||||
);
|
||||
groundToggle.button.title =
|
||||
state.ground === "soil"
|
||||
? "토사 — 암반이 없어 절토는 토사 경사 하나로만 그린다. 누르면 암으로 바뀐다."
|
||||
: "암 — 암 경계선이 서고 그 아래는 암 절토각, 위는 토사 경사로 그린다. 누르면 토사로 바뀐다.";
|
||||
const groundSegment = groundToggle.wrap;
|
||||
groundSegment.classList.add("b06-design__seg--header");
|
||||
// 단면 유형은 지형에서 자동 판정되며(D-2), 제목행 pill로 표시한다(E-3, Cross_View에서 생성).
|
||||
// 아래 옵션은 상시 노출하되 선행 조건 미충족 시 비활성 처리한다(E-6).
|
||||
|
||||
@@ -48,6 +48,7 @@ import { adoptAdjustPanel, releaseAdjustPanel } from "./B06_Section_UI_Adjust_Do
|
||||
import { culvertCardState, structurePanelDeps } from "./B06_Section_UI_Cross_View_Structure";
|
||||
import { attachZoomPan, buildZoomControls, cardZoomStates } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import type { CrossWidthActions } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
||||
import { toeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
||||
import { createCrossAxes, IDENTITY_VIEW } from "./B06_Section_UI_Cross_Axes";
|
||||
import type { RevetHighlightSetter, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
@@ -150,6 +151,8 @@ export function createCrossSectionCard(
|
||||
box?: BoxControl,
|
||||
/** 행 높이를 재며 이미 만들어 둔 기하 — 있으면 다시 계산하지 않는다(2026-09-06). */
|
||||
plotBase?: CrossPlotBase | null,
|
||||
/** 측점별 암 절토 경사 입력(2026-09-07) — 암 측점 하단 정보행에 선다. */
|
||||
cutSlope?: CutSlopeControl,
|
||||
): CrossCardElement {
|
||||
// 링크 카드 = 구조물이 옆 측점에 서 있고 그 연장이 여기까지 온 경우. 관만 숨기고
|
||||
// 선택·조정은 연다 — 연동을 풀어 이 측점 위치를 따로 잡을 수 있어야 한다
|
||||
@@ -704,7 +707,7 @@ export function createCrossSectionCard(
|
||||
card.append(chartWrap);
|
||||
}
|
||||
|
||||
appendCardFooter(card, section, rockBoundary);
|
||||
appendCardFooter(card, section, rockBoundary, cutSlope);
|
||||
return card;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { refreshCrossDesigns } from "./B06_Section_Cross_Refresh";
|
||||
import {
|
||||
bermSpansFromStructures,
|
||||
confirmCurrentSections,
|
||||
createCutSlopeStore,
|
||||
createRockBoundaryStore,
|
||||
readBermSpans,
|
||||
saveCurrentSections,
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
} from "./B06_Section_UI_Page_Pipe_Options";
|
||||
import {
|
||||
createStandardPanel,
|
||||
effectiveStandardCross,
|
||||
rememberRockBoundaryDefault,
|
||||
type StandardPanelController,
|
||||
} from "./B06_Section_UI_Standard_Panel";
|
||||
@@ -435,6 +437,16 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
const rockOffsets = rockStore.offsets;
|
||||
const rockBoundaryControl = rockStore.control;
|
||||
|
||||
// 측점별 암 절토 경사(2026-09-07 사용자 지시) — 암 경계선과 같은 꼴의 세션 저장소다.
|
||||
// 전체를 바꾸는 자리는 좌측 [표준 횡단면 설정]이고, 여기 값은 그 측점 하나만 덮는다.
|
||||
const cutSlopeStore = createCutSlopeStore({
|
||||
sessionKey: () => stateKey("cutslope", projectId, currentRouteId),
|
||||
standard: () => (projectId ? effectiveStandardCross(projectId) : null),
|
||||
refreshCard: (chainageM) => sectionView.refreshCard(chainageM),
|
||||
recompute: (chainageM) => recomputeIfRock(chainageM),
|
||||
});
|
||||
const cutSlopeRatios = cutSlopeStore.ratios;
|
||||
|
||||
const stationControls = createStationControls({
|
||||
// 키는 등록표(`b_page_state`)가 만든다 — 이름만 넘기면 통·범위·옛 키 이관이 따라온다.
|
||||
// 형변환을 두지 않는다: 등록표에 없는 이름을 쓰면 **컴파일에서** 걸린다
|
||||
@@ -464,6 +476,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
stationControls.revetLink,
|
||||
stationControls.ford,
|
||||
stationControls.box,
|
||||
cutSlopeStore.control,
|
||||
);
|
||||
// 좌측 목록이 넘겨 준 구조물을 종단 알약 레인으로 보낸다(표시 통일).
|
||||
structureMarksSink = (structures, types) => sectionView.setStructureMarks(structures, types);
|
||||
@@ -607,6 +620,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
flushCulvertOptions: () => stationControls.flushCulvertOptions(),
|
||||
patchSources: () => ({
|
||||
rockOffsets,
|
||||
cutSlopeRatios,
|
||||
stationWidths,
|
||||
inletStructures,
|
||||
basinAdjustments,
|
||||
@@ -677,6 +691,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
|
||||
currentRouteId = context.route_id;
|
||||
rockStore.load();
|
||||
cutSlopeStore.load();
|
||||
stationControls.load();
|
||||
// 구조물 배치 데이터(타입·정본·관 지점) — 카드 로드와 병행, 화면을 잠그지 않는다.
|
||||
void structuresPanel.load().then(restoreStructurePick);
|
||||
|
||||
@@ -16,6 +16,8 @@ import type { InletStructureChoice } from "./B06_Section_UI_Cross_Culvert";
|
||||
export interface CrossPatchSources {
|
||||
/** 암 경계선 오프셋(누가거리 문자열 키). */
|
||||
rockOffsets: Map<string, number>;
|
||||
/** 측점별 암 절토 경사비(1:n 의 n). **0 은 「표준값을 씀」**(되돌리기)이다. */
|
||||
cutSlopeRatios: Map<string, number>;
|
||||
/** 측점 개별 표시 반폭(m). */
|
||||
stationWidths: Map<string, number>;
|
||||
inletStructures: Map<string, InletStructureChoice>;
|
||||
@@ -42,6 +44,10 @@ export function buildCrossPatches(sources: CrossPatchSources): CrossSectionPatch
|
||||
sources.rockOffsets.forEach((offset, chainage) => {
|
||||
patchFor(Number(chainage)).rock_boundary_offset_m = offset;
|
||||
});
|
||||
// 측점별 암 절토 경사(2026-09-07) — 0 이면 「표준값을 씀」이라 정본의 옛 값을 덮어 지운다.
|
||||
sources.cutSlopeRatios.forEach((ratio, chainage) => {
|
||||
patchFor(Number(chainage)).cut_slope_ratio_user = ratio;
|
||||
});
|
||||
// 개별 표시 반폭(2026-08-06) — design에 병합돼 재접근 시 유지된다.
|
||||
sources.stationWidths.forEach((width, chainage) => {
|
||||
patchFor(Number(chainage)).display_half_width_m = width;
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { crossDesignChoices } from "./B06_Section_Cross_Design_Session";
|
||||
import type { StandardCrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { RockBoundaryControl } from "./B06_Section_UI_Section_View";
|
||||
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||
import { L } from "./B06_Section_UI_Page_Common";
|
||||
|
||||
@@ -249,6 +250,96 @@ export function createRockBoundaryStore(options: {
|
||||
};
|
||||
}
|
||||
|
||||
/** 측점별 암 절토 경사비 저장소 — 값(Map)과 카드 제어기를 함께 낸다(2026-09-07). */
|
||||
export interface CutSlopeStore {
|
||||
/** 측점키(누가거리 2자리) → 경사비(1:n 의 n). `buildCrossPatches` 가 그대로 읽는다.
|
||||
* **0 은 「표준값을 씀」**이다 — 되돌리기(↺)가 남기는 값이라, 저장분에 옛 사용자 값이
|
||||
* 남아 있어도 0 이 그것을 덮어 표준으로 돌려놓는다(지우기만 하면 옛 값이 되살아난다). */
|
||||
ratios: Map<string, number>;
|
||||
control: CutSlopeControl;
|
||||
load: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 측점 하나만 다른 **암 절토 경사** 세션 저장소.
|
||||
*
|
||||
* 암 경계선 저장소와 같은 꼴이다 — 세션(sessionStorage)에 쌓고 [저장]·[확정]에서
|
||||
* `cross_patches`(`design.cut_slope_ratio_user`)로 정본에 나간다.
|
||||
*
|
||||
* ⚠ 값을 넣으면 **재계산을 부른다**. 경사는 나르는 값이 아니라 기하 입력이라, 값만 바꾸고
|
||||
* 다시 계산하지 않으면 설계선은 옛 경사로 남고 숫자만 새것이 된다.
|
||||
*/
|
||||
export function createCutSlopeStore(options: {
|
||||
sessionKey: () => string | null;
|
||||
/** 지금 표준 횡단면 설정(세션 편집값 우선) — 되돌릴 자리를 여기서 읽는다. */
|
||||
standard: () => StandardCrossSection | null;
|
||||
refreshCard: (chainageM: number) => void;
|
||||
/** 경사 변경 → 사면·소단·단면적 재계산. */
|
||||
recompute: (chainageM: number) => void;
|
||||
}): CutSlopeStore {
|
||||
const { sessionKey, standard, refreshCard, recompute } = options;
|
||||
const ratios = new Map<string, number>();
|
||||
const key = (chainageM: number): string => chainageM.toFixed(2);
|
||||
|
||||
/** 이 측점이 쓰는 표준 경사비 — 지반 프리셋(암/토사)의 값. */
|
||||
const standardRatio = (section: { design?: { geometry_preset?: string } | null }): number => {
|
||||
const preset = section.design?.geometry_preset === "soil" ? "soil" : "rock";
|
||||
const group = standard()?.[preset] as { cut_slope_ratio?: number } | undefined;
|
||||
const value = group?.cut_slope_ratio;
|
||||
return typeof value === "number" && value > 0 ? value : 0.4;
|
||||
};
|
||||
|
||||
function persist(): void {
|
||||
const storageKey = sessionKey();
|
||||
if (!storageKey) return;
|
||||
try {
|
||||
writeByKey(storageKey, JSON.stringify(Object.fromEntries(ratios)));
|
||||
} catch {
|
||||
/* 세션 저장 실패는 무시(용량 초과 등) — 값은 메모리에 유지된다. */
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ratios,
|
||||
load(): void {
|
||||
ratios.clear();
|
||||
const storageKey = sessionKey();
|
||||
if (!storageKey) return;
|
||||
try {
|
||||
const raw = readByKey(storageKey);
|
||||
if (!raw) return;
|
||||
const parsed = JSON.parse(raw) as Record<string, number>;
|
||||
Object.entries(parsed).forEach(([chainage, ratio]) => {
|
||||
// 0(되돌림 표시)도 그대로 싣는다 — 거르면 저장분 옛 값이 되살아난다.
|
||||
if (Number.isFinite(ratio) && ratio >= 0) ratios.set(chainage, ratio);
|
||||
});
|
||||
} catch {
|
||||
/* 손상된 세션 값은 무시 — 표준값으로 재시작. */
|
||||
}
|
||||
},
|
||||
control: {
|
||||
standardRatioFor: (section) => standardRatio(section),
|
||||
ratioFor: (section) => {
|
||||
const session = ratios.get(key(section.chainage_m));
|
||||
if (session === 0) return standardRatio(section); // 되돌림 — 저장분보다 세션이 먼저다.
|
||||
if (typeof session === "number" && session > 0) return session;
|
||||
const stored = (section.design as { cut_slope_ratio_user?: number } | undefined)
|
||||
?.cut_slope_ratio_user;
|
||||
if (typeof stored === "number" && stored > 0) return stored;
|
||||
return standardRatio(section);
|
||||
},
|
||||
set: (chainageM, ratio) => {
|
||||
// null = 되돌리기. 지우지 않고 0 을 남겨야 저장분에 있던 옛 값까지 표준으로 돌아간다.
|
||||
if (ratio === null || !Number.isFinite(ratio) || ratio <= 0) ratios.set(key(chainageM), 0);
|
||||
else ratios.set(key(chainageM), Math.round(ratio * 10000) / 10000);
|
||||
persist();
|
||||
refreshCard(chainageM);
|
||||
recompute(chainageM);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** [저장]·[확정]이 함께 쓰는 페이지 상태 창구. */
|
||||
export interface SectionPersistContext {
|
||||
projectId: string | null;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* 외부(Page)에는 `createSectionView`와 `CrossDesignChange` 타입만 노출한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
@@ -146,6 +147,8 @@ export function createSectionView(
|
||||
revetLink?: RevetLinkControl,
|
||||
ford?: FordControl,
|
||||
box?: BoxControl,
|
||||
/** 측점별 암 절토 경사 입력(2026-09-07). */
|
||||
cutSlope?: CutSlopeControl,
|
||||
): SectionViewController {
|
||||
const root = document.createElement("div");
|
||||
root.className = "b06-section";
|
||||
@@ -457,6 +460,7 @@ export function createSectionView(
|
||||
ford,
|
||||
box,
|
||||
plotBase,
|
||||
cutSlope,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField, createSelectField } from "@ui/ui_template_elements";
|
||||
import { buildStandardDiagram } from "./B06_Section_UI_Standard_Diagram";
|
||||
import { ratioToDegrees } from "./B06_Section_UI_Cross_CutSlope";
|
||||
import {
|
||||
getCompanyStandard,
|
||||
listCompanyStandards,
|
||||
@@ -303,6 +304,19 @@ export function createStandardPanel(
|
||||
});
|
||||
field.input.step = "0.1";
|
||||
field.input.min = "0";
|
||||
// 경사 칸은 **각도도 함께 보인다** — 카드의 개별 절토각 칸이 도(°)로 받으므로 두 자리의
|
||||
// 말이 갈리지 않게 한다(2026-09-07). 1:0.4 = 68.2° · 1:1.0 = 45° · 1:1.5 = 33.7°.
|
||||
if (spec.label === "B06_Std_Field_CutSlope" || spec.label === "B06_Std_Field_FillSlope") {
|
||||
const showAngle = (): void => {
|
||||
const ratio = Number(field.input.value);
|
||||
field.input.title =
|
||||
Number.isFinite(ratio) && ratio > 0
|
||||
? `1:${ratio} = ${ratioToDegrees(ratio).toFixed(1)}° (횡단 카드의 각도 칸과 같은 값)`
|
||||
: "";
|
||||
};
|
||||
showAngle();
|
||||
field.input.addEventListener("input", showAngle);
|
||||
}
|
||||
grid.append(field.root);
|
||||
};
|
||||
|
||||
|
||||
@@ -572,3 +572,38 @@
|
||||
border-radius: var(--radius-inputs);
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
/* 측점별 암 절토 경사(2026-09-07) — 암 경계선이 행 가운데를 절대 배치로 쓰므로
|
||||
이 칸은 행의 **오른쪽 끝**에 둔다. 중심고(왼쪽)와 겹치지 않는다. */
|
||||
.b06-cross-card__cutslope {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
border-radius: var(--radius-inputs);
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.b06-design__cutslope-input {
|
||||
width: 44px;
|
||||
padding: 0 2px;
|
||||
font-size: 0.72rem;
|
||||
font-family: var(--font-mono);
|
||||
text-align: right;
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-inputs);
|
||||
}
|
||||
|
||||
/* 화살표(스피너)는 칸이 좁아 지운다 — 값은 직접 넣거나 ↺ 로 되돌린다. */
|
||||
.b06-design__cutslope-input::-webkit-outer-spin-button,
|
||||
.b06-design__cutslope-input::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.b06-design__cutslope-unit {
|
||||
padding: 0 2px 0 1px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-secondary);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ export interface CrossDesignOptions {
|
||||
standard: StandardCrossSectionSpec;
|
||||
rockBoundaryOffsetM?: number | null;
|
||||
twoStageSlope?: boolean;
|
||||
/** 이 측점만 쓰는 절토 경사비(1:n 의 n) — 카드에서 넣은 사용자 값. 없으면 표준값.
|
||||
* 짝: 파이썬 `compute_cross_design(cut_slope_ratio=…)`. */
|
||||
cutSlopeRatio?: number | null;
|
||||
ditchEnabled?: boolean | null;
|
||||
/** 세월교 월류 높이만큼 노면을 통째로 내린다(m). */
|
||||
surfaceDropM?: number;
|
||||
@@ -252,7 +255,14 @@ export function computeCrossDesign(
|
||||
if (ditchType === "l_type" && presetKey !== "rock") {
|
||||
throw new Error("L형 측구는 암(리핑암/발파암) 구간에서만 선택할 수 있습니다.");
|
||||
}
|
||||
const group = resolveGroup(presetKey, options.standard);
|
||||
let group = resolveGroup(presetKey, options.standard);
|
||||
// 측점 하나만 다른 절토 경사(2026-09-07 사용자 지시) — 표준 그룹의 경사비만 갈아 끼운다.
|
||||
// ⚠ 여기서 갈아야 아래 기하·소단이 전부 새 경사를 따른다. 계산이 끝난 뒤 값만 베껴
|
||||
// 붙이면 설계선은 옛 경사로 그려지고 숫자만 새것이 되어 어긋난다.
|
||||
const userCutRatio = options.cutSlopeRatio;
|
||||
if (typeof userCutRatio === "number" && Number.isFinite(userCutRatio) && userCutRatio > 0) {
|
||||
group = { ...group, cut_slope_ratio: userCutRatio };
|
||||
}
|
||||
const pavedGroup = resolveGroup("paved", options.standard);
|
||||
// 포장 중첩: 횡단경사와 포장층 두께만 포장 그룹을 따른다.
|
||||
const crossSlopePct = paved ? pavedGroup.cross_slope_pct : group.cross_slope_pct;
|
||||
|
||||
Reference in New Issue
Block a user