Merge remote-tracking branch 'origin/sub_laptop_5' into sub_laptop_1

This commit is contained in:
2026-10-19 07:22:37 +09:00
12 changed files with 799 additions and 41 deletions
+32 -5
View File
@@ -34,8 +34,15 @@ import {
readCutSlopeSession,
readRockBoundarySession,
} from "./B06_Section_UI_Page_Persist";
import type { BermSessionSpec } from "./B06_Section_UI_Page_Persist";
import type { BermSpec } from "@util/common_util_cross_berm";
import type { BermSessionSpec, BermSpan } from "./B06_Section_UI_Page_Persist";
import {
BERM_DEFAULT_DEPTH_M,
BERM_DEFAULT_INTERVAL_M,
BERM_DEFAULT_SLOPE_DEG,
BERM_DEFAULT_WIDTH_M,
type BermSpec,
} from "@util/common_util_cross_berm";
import { readState } from "../A00_Common/b_page_state";
import {
crossDesignChoices,
readBermDepths,
@@ -260,6 +267,24 @@ function bermPayload(
type Side = "left" | "right";
const SIDES: Side[] = ["left", "right"];
/** 저장분 소단 제원(`design.berm`) — 짝: 파이썬 `berm_from`(빈 칸 = 기본값 · 깊이 0 미만 = 0 · 못 읽으면 없음). */
export function storedBermSpec(berm: unknown): BermSessionSpec | null {
if (!berm || typeof berm !== "object" || Array.isArray(berm)) return null;
const spec = berm as Record<string, unknown>;
const value = (key: string, fallback: number): number => {
const raw = spec[key];
if (raw === undefined) return fallback;
return raw === null || raw === "" || typeof raw === "object" ? Number.NaN : Number(raw);
};
const out = {
width_m: value("width_m", BERM_DEFAULT_WIDTH_M),
interval_m: value("interval_m", BERM_DEFAULT_INTERVAL_M),
slope_deg: value("slope_deg", BERM_DEFAULT_SLOPE_DEG),
depth_m: Math.max(value("depth_m", BERM_DEFAULT_DEPTH_M), 0),
};
return Object.values(out).every(Number.isFinite) ? out : null;
}
/** 소단 쪽마다 옮긴 깊이(194-1) — 세션 → 저장분 순 · 없으면 null(구간 깊이를 씀). */
function bermDepthReader(projectId: string, routeId: number) {
const session = readBermDepths(projectId, routeId);
@@ -333,11 +358,13 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
// 그려진다. 계산 뒤에 값만 베껴 붙이면 그림은 옛 경사, 숫자만 새것이 된다(2026-09-07).
const cutSlopeRatios = readCutSlopeRatios(projectId, input.routeId);
// 소단도 같은 성격 — 세션에만 있는 값이라 여기서 실어 주지 않으면 계획선을 고치는
// 순간 계단이 사라진다(계획서 3-9).
const bermSpans = readBermSpans(projectId, input.routeId);
// 순간 계단이 사라진다(계획서 3-9). 사본 없음(시설 줄이 구조물을 받기 전) = 저장분 `design.berm`
// (파이썬 `load_berm_spans` 못 읽음 → 저장분 · `stored_berm` 짝 · 467).
const bermCopy = readState<BermSpan[]>("berm", projectId, input.routeId);
const bermSpans = Array.isArray(bermCopy) ? bermCopy : null;
const bermDepthAt = bermDepthReader(projectId, input.routeId);
const bermAt = (chainageM: number, design: Record<string, unknown>): BermSpec | null => {
const spec = bermSpecAt(bermSpans, chainageM);
const spec = bermSpans ? bermSpecAt(bermSpans, chainageM) : storedBermSpec(design.berm);
return spec
? {
widthM: spec.width_m,
+8 -1
View File
@@ -41,6 +41,7 @@ import { staleKeyAgainstScreen, USER_TOUCHED_KEYS } from "./B06_Section_Cross_Re
import { kindChangedChainages, STRUCTURE_ADJUST_KEYS } from "./B06_Section_Api_Culvert_Options";
import type { PipelineStation } from "../A00_Common/design/logic/design_pipeline_types";
import type { DesignStoreValues } from "../A00_Common/design/design_store";
import { useDesignStore } from "../A00_Common/design/design_store";
import {
carryStationKeys,
KEYED_STATES,
@@ -237,7 +238,13 @@ export async function swapSectionStations(
const interval = stationIntervalOf(current);
const oldKeys = registeredKeys(projectId, routeId, current.cross_sections, interval);
if (sameStationList(current.cross_sections, oldKeys, stations)) return null;
const swap = swapCrossSections(current, stations, oldKeys, interval);
const swap = swapCrossSections(
current,
stations,
oldKeys,
interval,
useDesignStore(projectId, null).get("routeDraft") !== null,
); // 초안 없으면 정본 표본(467)
// 설계는 사본 — 면적 얹기 · 다시 셈이 앞 상세(초안 전 상세 등)의 설계를 제자리로 안 고침(349-0o)
for (const section of swap.detail.cross_sections)
if (section.design) section.design = { ...section.design };
+4 -19
View File
@@ -5,7 +5,7 @@
* - `handleDesignChange` : 버튼 선택 즉시 반영 + 세션 초안 기록 + 재계산
* - `recomputeIfRock` : 암 경계·암 절토 경사 변경 후 암 측점만 재계산
* - `reconcileStaleDesigns`: 횡단 다시 셈을 계산 묶음에 부탁(cross 단계 한 곳 · 387-4)
* - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기
* - `syncBermSpans` : 구조물 측점 열쇠(소단 사본은 페이지 시설 줄이 씀 · 467)
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
* 고친 뒤 계산 묶음 cross 입력도 다시(`pipelineCross` · 노선 초안 동안 카드 = 묶음 새 노선 상세 · 365-2).
* ========================================================================== */
@@ -15,11 +15,6 @@ import {
writeCrossDesignChoice,
writeShoulderExtend,
} from "./B06_Section_Cross_Design_Session";
import {
bermSpansFromStructures,
readBermSpans,
writeBermSpans,
} from "./B06_Section_UI_Page_Persist";
import { showToast } from "@ui/ui_template_elements";
import { L } from "./B06_Section_UI_Page_Common";
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
@@ -137,20 +132,10 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
await reconcilePagePipeline(projectId);
}
/**
* 구조물 목록의 **소단**(C군 사면안정)을 세션 사본으로 편다 — 재계산이 측점마다 읽는 값이다.
*
* 사용자는 「구조물 배치」에서 놓고(2026-09-07 확정), 계산은 그 사본만 본다. 달라졌을 때만
* 다시 계산한다 — 목록은 화면을 열 때도 오므로 매번 돌리면 진입이 느려진다.
*/
/** 구조물 측점 열쇠 = 구조물 id(344-3) — 소단 사본(세션 `berm`)은 페이지 시설 줄 한 곳이 씀(B05 만 열어도 · 467). */
function syncBermSpans(structures: ReadonlyArray<StructureInstance>): void {
const currentRouteId = ctx.routeId();
if (!projectId || currentRouteId === null) return;
useStructureIds(projectId, structures); // 구조물 측점 열쇠 = 구조물 id(344-3)
const next = bermSpansFromStructures(structures);
if (JSON.stringify(next) === JSON.stringify(readBermSpans(projectId, currentRouteId))) return;
writeBermSpans(projectId, currentRouteId, next);
void reconcileStaleDesigns({ force: true });
if (!projectId || ctx.routeId() === null) return;
useStructureIds(projectId, structures);
}
/** 패널 [전체 반영](N-2-1 · 346-2): 표준값으로 전 측점을 **한 번** 다시 셈 — 측점마다 돌면
+38 -13
View File
@@ -6,6 +6,8 @@
* 받음 — 설계 · 구조물 제원 · 이름 그대로 · 자리(누가거리 · 좌표 · 방위 · 확폭)만 새것(433-4).
* · 지반 표본 · 상단측 · 중심고 = 자리 · 축이 같은 앞 횡단이면 앞 값(서버 샘플링 정본 · 덧 측점만 넣어도 나머지
* 측점 면적이 브라우저 지형으로 안 바뀜 · 433-11) · 새 측점 · 옮긴 측점만 지반 표본(채워 왔으면).
* · 노선 초안 없는 차례(관 · 구조물 · 추가 측점만) = 같은 열쇠 · 누가거리 0.01 m 안 앞 횡단의 기하 · 표본 그대로
* (축 허용 1e-3 · 브라우저 측점 틀 ↔ 서버 틀 mm 차가 정본 표본을 안 덮음 · 467 · 요구 272).
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
* · 상단측 = 새 지반 표본으로 다시 셈한 측점은 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
* · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자 칸(만진 칸 · 지반 등 입력 `ROW_CARRY_KEYS`) — 서버가
@@ -146,27 +148,31 @@ export interface StationSwap {
moved: boolean;
}
/** 앞 횡단과 자리(누가거리 · 좌표 0.01 m) · 축(왼쪽 단위 벡터)이 같은가 — 같으면 앞 지반 표본을 그대로 씀. */
function samePlace(prior: CrossSection, station: PipelineStation): boolean {
/** 앞 횡단과 자리(누가거리 · 좌표 0.01 m) · 축(왼쪽 단위 벡터 · 허용 `axisTol`)이 같은가 — 같으면 앞 지반 표본을 그대로 씀. */
function samePlace(prior: CrossSection, station: PipelineStation, axisTol = 1e-4): boolean {
const [lx, ly] = prior.frame?.left_xy ?? [Number.NaN, Number.NaN];
return (
Math.abs(prior.chainage_m - station.chainageM) < 0.01 &&
Math.hypot(prior.center_x - station.x, prior.center_y - station.y) <= 0.01 &&
Math.abs(lx - station.left[0]) < 1e-4 &&
Math.abs(ly - station.left[1]) < 1e-4
Math.abs(lx - station.left[0]) < axisTol &&
Math.abs(ly - station.left[1]) < axisTol
);
}
/** 노선 초안 없는 차례의 축 허용(측점 축 0.06° · 20 m 끝 2 cm) — 브라우저 틀 ↔ 서버 틀 차(R 12 곡선 3.3e-4)를 넘김. */
const CANON_AXIS_TOL = 1e-3;
/**
* 새 측점 목록으로 상세를 새로 짬. `oldKeys` = 지금 상세 측점마다 열쇠(같은 차례 · `stationKeysOf`) ·
* 열쇠가 안 맞으면 옮긴 곡선 열쇠(`curveMoves` · 서버 `carry_cross_inputs` 같은 규칙) · 그다음 같은 자리(0.01 m) ·
* 같은 종류의 앞 횡단을 씀.
* 같은 종류의 앞 횡단을 씀. `routeDraft` = 노선 초안 차례(가게 `routeDraft` · 아니면 정본 기하 · 표본 유지 · 467).
*/
export function swapCrossSections(
detail: SectionDetailResponse,
stations: ReadonlyArray<PipelineStation>,
oldKeys: ReadonlyArray<string>,
intervalM: number | null,
routeDraft = false,
): StationSwap {
const byKey = new Map(detail.cross_sections.map((section, i) => [oldKeys[i], section]));
const used = new Set<CrossSection>();
@@ -209,8 +215,15 @@ export function swapCrossSections(
moved = true;
if (prior && text(prior.chainage_m) !== text(station.chainageM))
moves.set(text(prior.chainage_m), text(station.chainageM));
// 노선 초안 없는 차례 · 같은 열쇠 · 같은 자리 = 앞 횡단 기하 · 표본 그대로(467 · 되돌리면 정본과 같음)
const canon =
!routeDraft &&
!!prior &&
byKey.get(station.key) === prior &&
samePlace(prior, station, CANON_AXIS_TOL);
// 자리 같은 앞 횡단 = 서버 표본 그대로(433-11) · 아니면 새 지반 표본(채워 왔으면)
const fresh = !!station.ground && !(prior?.samples?.length && samePlace(prior, station));
const fresh =
!canon && !!station.ground && !(prior?.samples?.length && samePlace(prior, station));
const kind = kindOf(station);
const onGrid =
kind === "irregular" &&
@@ -233,13 +246,25 @@ export function swapCrossSections(
center_z: fresh
? (station.ground?.centerZ ?? prior?.center_z ?? null)
: (prior?.center_z ?? null),
azimuth_deg: station.azimuthDeg,
plan_radius_m: station.planRadiusM,
curve_outer_side: station.curveOuterSide,
curve_widening_m: station.curveWideningM,
center_x: station.x,
center_y: station.y,
frame: { left_xy: [station.left[0], station.left[1]] as [number, number] },
...(canon && prior
? {
azimuth_deg: prior.azimuth_deg,
plan_radius_m: prior.plan_radius_m,
curve_outer_side: prior.curve_outer_side,
curve_widening_m: prior.curve_widening_m,
center_x: prior.center_x,
center_y: prior.center_y,
frame: prior.frame,
}
: {
azimuth_deg: station.azimuthDeg,
plan_radius_m: station.planRadiusM,
curve_outer_side: station.curveOuterSide,
curve_widening_m: station.curveWideningM,
center_x: station.x,
center_y: station.y,
frame: { left_xy: [station.left[0], station.left[1]] as [number, number] },
}),
};
longStations.push({ ...place, ...(prior?.structure ? { structure: prior.structure } : {}) });
const samples = fresh ? groundSamples(station) : (prior?.samples ?? []);