Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -6,7 +6,9 @@
|
||||
* 묶음 측점과 같은 열쇠 규칙(관 = 저장 관 id). 초안 자리로 뜬 시설 측점 = 등록 열쇠(주인 id)로 짝(457).
|
||||
*
|
||||
* 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 ·
|
||||
* 암 경계 · 암 경사 · 소단) + 사면 쉬프트(노견 확장) · 소단 깊이(417 · 서버 `ROUTE_CARRY_KEYS`) — 사용자 칸만.
|
||||
* 암 경계 · 암 경사 · 소단) + 사면 쉬프트(노견 확장) · 소단 깊이(417 · 서버 `ROUTE_CARRY_KEYS`) + 손 상단측(단면
|
||||
* 유형 · 측구 방향) — 사용자 칸만. 저장분의 자리 칸(`SEAT_KEYS` · 암 경계 · 2단 경사 · 손 상단측)은 저장 측점 자리를
|
||||
* 함께 실어(`seat`) 새 자리로 간 측점에서 뺌(`crossAtSeat` · 요구 275-3) · 세션 고침은 끌기 넘김이 이미 가름.
|
||||
* 그림 자료 · 카드 이름 · 얹힌 시설 맥락(포장 · 기울임 · 하강) · 단면 유형 · 측구 방향은 새 자리에서 다시 셈
|
||||
* (`logic/B05_Profile_Pipeline_Detail.ts` `crossWithShapes` · `draftSectionDetail` · `crossWithUphill` · 457). 값 = 상세에 얹힌 설계 위에 세션 고침(측점 열쇠 칸 ·
|
||||
* 지반 유형 · 측구 · 2단 경사 · 암 경계 · 암 경사 · 사면 쉬프트 · 소단 깊이) — 노선 초안 동안 B06 고침은 저장 상세에
|
||||
@@ -20,8 +22,14 @@ import {
|
||||
effectiveStandardCross,
|
||||
readRockBoundaryDefault,
|
||||
} from "../../B06_Section/B06_Section_UI_Standard_Panel";
|
||||
import type { CrossInput, SectionSpecs } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import type {
|
||||
CrossInput,
|
||||
SeatedFields,
|
||||
SectionSpecs,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import { SEAT_HINT } from "../../B06_Section/logic/B06_Section_Route_Carry";
|
||||
import type { PipelineStation } from "./logic/design_pipeline_types";
|
||||
import type { StationSeat } from "../../common_util/common_util_station_key";
|
||||
|
||||
export interface PipelineCrossSource {
|
||||
input: CrossInput;
|
||||
@@ -47,6 +55,15 @@ export const forceCross = (): void => void (forced += 1);
|
||||
const finite = (value: unknown): number | null =>
|
||||
typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
|
||||
/** 저장분 자리 칸의 입력 이름(`SEAT_KEYS` 짝 · 요구 275-3). */
|
||||
const SEAT_INPUTS = [
|
||||
"twoStageSlope",
|
||||
"rockBoundaryOffsetM",
|
||||
"cutSlopeRatio",
|
||||
"sectionMode",
|
||||
"ditchSide",
|
||||
] as const;
|
||||
|
||||
/** 측점 하나의 넘기는 사용자 입력(cross 단계 `CrossStationInput` 꼴 · 시설 맥락은 새 자리 그림에서 · 457). */
|
||||
function carried(section: CrossSection, saved?: CrossSection["design"]): Record<string, unknown> {
|
||||
const design = (saved ?? section.design ?? {}) as unknown as Record<string, unknown>;
|
||||
@@ -62,6 +79,8 @@ function carried(section: CrossSection, saved?: CrossSection["design"]): Record<
|
||||
twoStageSlope: Boolean(design.two_stage_slope),
|
||||
rockBoundaryOffsetM: finite(design.rock_boundary_offset_m),
|
||||
cutSlopeRatio: cutSlope !== null && cutSlope > 0 ? cutSlope : null,
|
||||
...(typeof design.section_mode === "string" ? { sectionMode: design.section_mode } : {}),
|
||||
...(typeof design.ditch_side === "string" ? { ditchSide: design.ditch_side } : {}),
|
||||
berm: berm
|
||||
? {
|
||||
widthM: berm.width_m,
|
||||
@@ -89,21 +108,29 @@ function sides(design: Record<string, unknown>, field: string, name: string) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 세션 칸 하나를 측점 열쇠마다 — 옛 열쇠(누가거리 · `at:`)는 읽는 쪽이 옮긴 뒤에 들어옴(빼고 봄). */
|
||||
/** 세션 칸 하나를 측점 열쇠마다 [열쇠, 값, 쓴 때 자리] — 옛 열쇠(누가거리 · `at:`)는 읽는 쪽이 옮긴 뒤에 들어옴(빼고 봄). */
|
||||
function sessionCells(
|
||||
name: "crossdesign" | "rockb" | "cutslope" | "bermdepth" | "shoulderext",
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
): Array<[string, unknown]> {
|
||||
): Array<[string, unknown, StationSeat | undefined]> {
|
||||
const raw = readState<Record<string, unknown>>(name, projectId, routeId);
|
||||
if (!raw || typeof raw !== "object") return [];
|
||||
return Object.entries(raw).filter(([key]) => key.includes(":") && !key.startsWith("at:"));
|
||||
const seats = (raw[SEAT_HINT] ?? {}) as Record<string, StationSeat>;
|
||||
return Object.entries(raw)
|
||||
.filter(([key]) => key.includes(":") && !key.startsWith("at:"))
|
||||
.map(([key, value]) => [key, value, seats[key]]);
|
||||
}
|
||||
|
||||
/** 세션 횡단 고침 → 측점 열쇠마다 넘기는 입력 조각(`carried` 와 같은 칸 이름). */
|
||||
/** 이 자리에서 온 자리 칸 이름을 입력 조각에 붙임(`crossAtSeat` 가 새 자리면 뺌 · 요구 275-3). */
|
||||
function seatOn(entry: Record<string, unknown>, seat: StationSeat | undefined, fields: string[]) {
|
||||
if (seat && fields.length) ((entry.seats ??= []) as SeatedFields[]).push({ ...seat, fields });
|
||||
}
|
||||
|
||||
/** 세션 횡단 고침 → 측점 열쇠마다 넘기는 입력 조각(`carried` 와 같은 칸 이름 · 자리 칸은 쓴 때 자리째). */
|
||||
function sessionCarried(projectId: string, routeId: number): CrossInput["byKey"] {
|
||||
const out: CrossInput["byKey"] = {};
|
||||
for (const [key, value] of sessionCells("crossdesign", projectId, routeId)) {
|
||||
for (const [key, value, seat] of sessionCells("crossdesign", projectId, routeId)) {
|
||||
const choice = (value ?? {}) as Record<string, unknown>;
|
||||
const entry = (out[key] ??= {});
|
||||
if (typeof choice.ground_type === "string") entry.groundType = choice.ground_type;
|
||||
@@ -111,12 +138,26 @@ function sessionCarried(projectId: string, routeId: number): CrossInput["byKey"]
|
||||
if ("ditch_choice" in choice)
|
||||
entry.ditchChoice = typeof choice.ditch_choice === "boolean" ? choice.ditch_choice : null;
|
||||
if ("two_stage_slope" in choice) entry.twoStageSlope = Boolean(choice.two_stage_slope);
|
||||
if (typeof choice.section_mode === "string") entry.sectionMode = choice.section_mode;
|
||||
if (typeof choice.ditch_side === "string") entry.ditchSide = choice.ditch_side;
|
||||
seatOn(
|
||||
entry,
|
||||
seat,
|
||||
["twoStageSlope", "sectionMode", "ditchSide"].filter((name) => name in entry),
|
||||
);
|
||||
}
|
||||
for (const [key, value] of sessionCells("rockb", projectId, routeId))
|
||||
if (finite(value) !== null) (out[key] ??= {}).rockBoundaryOffsetM = value;
|
||||
for (const [key, value] of sessionCells("cutslope", projectId, routeId)) {
|
||||
for (const [key, value, seat] of sessionCells("rockb", projectId, routeId))
|
||||
if (finite(value) !== null) {
|
||||
const entry = (out[key] ??= {});
|
||||
entry.rockBoundaryOffsetM = value;
|
||||
seatOn(entry, seat, ["rockBoundaryOffsetM"]);
|
||||
}
|
||||
for (const [key, value, seat] of sessionCells("cutslope", projectId, routeId)) {
|
||||
const ratio = finite(value);
|
||||
if (ratio !== null) (out[key] ??= {}).cutSlopeRatio = ratio > 0 ? ratio : null;
|
||||
if (ratio === null) continue;
|
||||
const entry = (out[key] ??= {});
|
||||
entry.cutSlopeRatio = ratio > 0 ? ratio : null;
|
||||
seatOn(entry, seat, ["cutSlopeRatio"]);
|
||||
}
|
||||
// 소단 쪽 깊이(194-1) — 소단 제원은 구조물 구간이 정함(`crossWithBerm` · 365-5) · 사면 쉬프트(139-6 · 417 M3)
|
||||
for (const [state, name] of [
|
||||
@@ -147,12 +188,20 @@ export function pipelineCrossSource(
|
||||
if (!detail || !standard) return null;
|
||||
const byKey: CrossInput["byKey"] = {};
|
||||
const specs = new Map<string, SectionSpecs>();
|
||||
/** 저장분을 실은 측점 열쇠 → 저장 측점 자리(자리 칸 판정 · 요구 275-3). */
|
||||
const seats = new Map<string, StationSeat>();
|
||||
// 설계 칸 = 세션에 남긴 저장 상세(초안 · 다시 셈이 안 고친 정본 — 들어온 화면 · 고친 차례 무관 · 365-5)
|
||||
const stored = routeId === null ? null : savedDesigns(projectId, routeId); // 세션 다시 안 풂(387-7)
|
||||
const storedAt = (chainageM: number) =>
|
||||
stored?.find((row) => Math.abs(row.chainage_m - chainageM) < SAME_M)?.design;
|
||||
const put = (key: string, section: CrossSection, at = section.chainage_m): void => {
|
||||
const put = (
|
||||
key: string,
|
||||
section: CrossSection,
|
||||
owner: PipelineStation,
|
||||
at = section.chainage_m,
|
||||
): void => {
|
||||
byKey[key] = carried(section, storedAt(at));
|
||||
seats.set(key, { x: owner.x, y: owner.y, left: owner.left }); // 저장 측점 자리
|
||||
// 카드 번호 = 저장 카드 번호(같은 측점 열쇠 · 옮긴 관도 · 찾기 · 고름이 같은 id · 440)
|
||||
specs.set(key, { station_id: section.station_id });
|
||||
};
|
||||
@@ -164,7 +213,7 @@ export function pipelineCrossSource(
|
||||
if (gap < SAME_M && (!best || gap < Math.abs(best.chainageM - section.chainage_m)))
|
||||
best = station;
|
||||
}
|
||||
if (best && !byKey[best.key]) put(best.key, section);
|
||||
if (best && !byKey[best.key]) put(best.key, section, best);
|
||||
else if (!best && section.kind === "irregular") strays.push(section);
|
||||
}
|
||||
// 초안 자리로 미리 뜬 관 · 구조물 측점(초안 둔 채 열기 — 상세가 초안 누가거리) = 등록 열쇠(주인 id)가 같은 저장
|
||||
@@ -178,11 +227,19 @@ export function pipelineCrossSource(
|
||||
const owner = savedStations.find(
|
||||
(station) => station.key === key && (station.kind === "pipe" || station.kind === "structure"),
|
||||
);
|
||||
if (owner && !byKey[key]) put(key, section, owner.chainageM);
|
||||
if (owner && !byKey[key]) put(key, section, owner, owner.chainageM);
|
||||
}
|
||||
const session = routeId === null ? {} : sessionCarried(projectId, routeId);
|
||||
for (const [key, entry] of Object.entries(session)) byKey[key] = { ...byKey[key], ...entry };
|
||||
// 저장분에서 온 자리 칸 = 저장 측점 자리째(세션이 고친 칸은 세션 값과 그 자리)
|
||||
for (const [key, seat] of seats) {
|
||||
const own = session[key] ?? {};
|
||||
seatOn(
|
||||
byKey[key],
|
||||
seat,
|
||||
SEAT_INPUTS.filter((name) => byKey[key][name] !== undefined && !(name in own)),
|
||||
);
|
||||
}
|
||||
if (routeId !== null)
|
||||
for (const [key, entry] of Object.entries(sessionCarried(projectId, routeId)))
|
||||
byKey[key] = { ...byKey[key], ...entry };
|
||||
return {
|
||||
// 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p)
|
||||
// 카드 번호 지문(365-5 · 시설 그림이 바뀌면 관 · 구조물 입력이 cross 를 다시 돌림 · 457)
|
||||
|
||||
@@ -20,6 +20,7 @@ import { groundStageDeps } from "../../B05_Profile/B05_Profile_Api_TerrainGrid";
|
||||
import { createGroundStage } from "../../B05_Profile/logic/B05_Profile_Pipeline_RouteGround";
|
||||
import {
|
||||
crossWithShapes,
|
||||
crossAtSeat,
|
||||
crossWithUphill,
|
||||
shapeStatus,
|
||||
type CrossInput,
|
||||
@@ -81,7 +82,11 @@ export async function pipelineCrossAtChainage(
|
||||
inputs.structures,
|
||||
await fetchStructureTypes(true).catch(() => []),
|
||||
);
|
||||
const shaped = crossWithShapes(inputs.cross as CrossInput | null, [station], status);
|
||||
const shaped = crossWithShapes(
|
||||
crossAtSeat(inputs.cross as CrossInput | null, [station]),
|
||||
[station],
|
||||
status,
|
||||
);
|
||||
const out = {
|
||||
profile,
|
||||
ground: { stations: [station], longitudinal: { chainageM: [], z: [] } },
|
||||
|
||||
@@ -49,6 +49,7 @@ from B05_Profile.B05_Profile_Route_Save_Parts import (
|
||||
route_moved_stations,
|
||||
seat_route_moved,
|
||||
station_keys,
|
||||
station_seats,
|
||||
vertices_of,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Schema import RouteReplanRequest, RouteVertexInput
|
||||
@@ -277,6 +278,7 @@ async def _carry_cross(
|
||||
from B06_Section.B06_Section_Router_Confirm import _rowless_station_chainages
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
CARRY_INPUT_KEYS,
|
||||
SEAT_KEYS,
|
||||
STRUCTURE_ADJUST_KEYS,
|
||||
compute_default_designs,
|
||||
)
|
||||
@@ -292,8 +294,19 @@ async def _carry_cross(
|
||||
for row in old_designs
|
||||
if isinstance(row.get("design"), dict)
|
||||
}
|
||||
# 자리 칸(암 경계 · 2단 경사 · 손 상단측)은 같은 자리 측점만(요구 275-3)
|
||||
seats = (
|
||||
station_seats(old_long.get("stations") or [], old_keys),
|
||||
station_seats(new_long.get("stations") or [], new_keys),
|
||||
)
|
||||
inputs, adjusts, dropped = carry_cross_inputs(
|
||||
old_keys, new_keys, designs, (*CARRY_INPUT_KEYS, *ROUTE_CARRY_KEYS), STRUCTURE_ADJUST_KEYS
|
||||
old_keys,
|
||||
new_keys,
|
||||
designs,
|
||||
(*CARRY_INPUT_KEYS, *ROUTE_CARRY_KEYS),
|
||||
STRUCTURE_ADJUST_KEYS,
|
||||
SEAT_KEYS,
|
||||
seats,
|
||||
)
|
||||
chainages = await asyncio.to_thread(_rowless_station_chainages, project_root, relative, [])
|
||||
entries = await asyncio.to_thread(
|
||||
|
||||
@@ -32,7 +32,12 @@ from common_util.common_util_drainage_pipes import (
|
||||
)
|
||||
from common_util.common_util_route_geometry import RouteVertex
|
||||
from common_util.common_util_station_key import chainage_key as station_chainage_key
|
||||
from common_util.common_util_station_key import curve_moves, station_key
|
||||
from common_util.common_util_station_key import (
|
||||
curve_moves,
|
||||
same_seat,
|
||||
station_key,
|
||||
station_seat,
|
||||
)
|
||||
from common_util.common_util_structure_station import plan_stations, snap_structure_station
|
||||
from config.config_system import DRAINAGE_PIPE_MIN_SPACING_M
|
||||
|
||||
@@ -153,20 +158,36 @@ def station_keys(
|
||||
return out
|
||||
|
||||
|
||||
def station_seats(
|
||||
stations: Sequence[dict[str, Any]], keys: dict[str, float]
|
||||
) -> dict[str, tuple[float, float, tuple[float, float] | None] | None]:
|
||||
"""측점 열쇠 → 그 측점 자리(`station_seat` · 같은 누가거리 0.001 m 줄 · 모르면 None)."""
|
||||
rows = {
|
||||
round(float(row["chainage_m"]), 3): row
|
||||
for row in stations
|
||||
if isinstance(row, dict) and row.get("chainage_m") is not None
|
||||
}
|
||||
return {key: station_seat(rows.get(round(at, 3))) for key, at in keys.items()}
|
||||
|
||||
|
||||
def carry_cross_inputs(
|
||||
old_keys: dict[str, float],
|
||||
new_keys: dict[str, float],
|
||||
old_designs: dict[float, dict[str, Any]],
|
||||
input_keys: Sequence[str],
|
||||
adjust_keys: Sequence[str],
|
||||
seat_keys: Sequence[str] = (),
|
||||
seats: tuple[dict[str, Any], dict[str, Any]] | None = None,
|
||||
) -> tuple[dict[float, dict[str, Any]], dict[float, dict[str, Any]], int]:
|
||||
"""같은 열쇠 측점으로 횡단 입력을 넘김 → (입력{새 누가거리 0.001: 값}, 구조물 조정{…}, 버린 수).
|
||||
|
||||
`old_designs` = 옛 노선 행 {누가거리 0.001: design}. 버린 수 = 설계가 있던 옛 측점 중 새 노선에
|
||||
같은 열쇠가 없는 것(「노선 밖 고침」) · MC 가 움직인 곡선 열쇠는 가까운 새 곡선 측점
|
||||
(`curve_moves`).
|
||||
(`curve_moves`). 자리 칸(`seat_keys` · 요구 275-3)은 옛 · 새 자리(`seats` = 열쇠 → 자리)가
|
||||
같은(`same_seat`) 측점만 넘김 · 새 자리 = 기본값(TS `carryKeyedStates` 와 같은 규칙).
|
||||
"""
|
||||
moves = curve_moves(list(old_keys.items()), list(new_keys.items()))
|
||||
old_seats, new_seats = seats or ({}, {})
|
||||
inputs: dict[float, dict[str, Any]] = {}
|
||||
adjusts: dict[float, dict[str, Any]] = {}
|
||||
dropped = 0
|
||||
@@ -174,11 +195,15 @@ def carry_cross_inputs(
|
||||
design = old_designs.get(round(old_at, 3))
|
||||
if not isinstance(design, dict):
|
||||
continue
|
||||
new_at = new_keys.get(moves.get(key, key))
|
||||
new_key = moves.get(key, key)
|
||||
new_at = new_keys.get(new_key)
|
||||
if new_at is None:
|
||||
dropped += 1
|
||||
continue
|
||||
carried = {name: design[name] for name in input_keys if design.get(name) is not None}
|
||||
seated = same_seat(old_seats.get(key), new_seats.get(new_key))
|
||||
names = [n for n in input_keys if seated or n not in seat_keys]
|
||||
names += [n for n in seat_keys if seated and n not in names]
|
||||
carried = {name: design[name] for name in names if design.get(name) is not None}
|
||||
if carried:
|
||||
inputs[round(new_at, 3)] = carried
|
||||
if key.startswith(("pipe:", "structure:")):
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* crossWithUphill cross 단계 입력에 단면 유형 기본값(상단측)을 얹음 — 서버 노선 저장이 단면 유형 ·
|
||||
* 측구 방향을 새 지형으로 정하는 것과 같은 규칙(넘기는 칸 = 부르는 쪽 입력).
|
||||
* crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457).
|
||||
* crossAtSeat 자리 칸(암 경계 · 2단 경사 · 손 상단측 · `seats`)은 측점이 그 자리 그대로일 때만 · 새 자리 =
|
||||
* 기본값(요구 275-3 · 판정 `sameSeat` · 서버 노선 저장 · Node 넘김과 같은 규칙).
|
||||
* draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 그림 자료 · 카드 이름 = 지금
|
||||
* 현황을 새 측점에 다시 얹음 · 캐시 `withDraftShapes` 와 같은 `attachShapes` · 457).
|
||||
*
|
||||
@@ -28,6 +30,7 @@ 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 { sameSeat, type StationSeat } from "../../common_util/common_util_station_key";
|
||||
import { attachShapes, type AttachType } from "../../common_util/common_util_structure_attach";
|
||||
import { registryDefaults } from "../../common_util/common_util_culvert_sets";
|
||||
import {
|
||||
@@ -74,6 +77,27 @@ export function uphillSide(station: PipelineStation): "left" | "right" | null {
|
||||
return gap > 0 ? "left" : "right";
|
||||
}
|
||||
|
||||
/** 입력 조각의 자리 칸 묶음 — 이 자리(저장 측점 · 세션 쓴 때)에서 온 입력 이름. */
|
||||
export type SeatedFields = StationSeat & { fields: string[] };
|
||||
|
||||
/** 자리 칸을 측점 자리로 가름 — 묶음(`seats`)의 자리와 지금 측점 자리가 다르면 그 입력을 뺌(기본값) · 묶음은 걷음. */
|
||||
export function crossAtSeat(
|
||||
input: CrossInput | null,
|
||||
stations: ReadonlyArray<PipelineStation>,
|
||||
): CrossInput | null {
|
||||
if (!input) return null;
|
||||
const byKey: CrossInput["byKey"] = { ...input.byKey };
|
||||
for (const station of stations) {
|
||||
const { seats, ...rest } = byKey[station.key] ?? {};
|
||||
if (!Array.isArray(seats)) continue;
|
||||
const here = { x: station.x, y: station.y, left: station.left };
|
||||
for (const seat of seats as SeatedFields[])
|
||||
if (!sameSeat(seat, here)) for (const name of seat.fields) delete rest[name];
|
||||
byKey[station.key] = rest;
|
||||
}
|
||||
return { ...input, byKey };
|
||||
}
|
||||
|
||||
/** 측점마다 단면 유형 기본값(상단측 깎기 · 모르면 좌) — 입력이 정한 단면 유형은 그대로. */
|
||||
export function crossWithUphill(
|
||||
input: CrossInput | null,
|
||||
@@ -274,7 +298,7 @@ export const crossWithContext = (
|
||||
bermAt: BermAt | null,
|
||||
): CrossInput | null =>
|
||||
crossWithBerm(
|
||||
crossWithUphill(crossWithShapes(input, stations, status), stations),
|
||||
crossWithUphill(crossWithShapes(crossAtSeat(input, stations), stations, status), stations),
|
||||
stations,
|
||||
bermAt,
|
||||
);
|
||||
|
||||
@@ -115,16 +115,20 @@ STRUCTURE_ADJUST_KEYS = (
|
||||
# 조작값은 넘기지 않는다. 받는 쪽은 `compute_default_designs(carried=…)`.
|
||||
# 칸 목록 정본 = TS `ROW_CARRY_KEYS`(logic/B06_Section_Station_Swap.ts) — 그 글을 읽음(472-4 ① X6).
|
||||
_SWAP_TS = Path(__file__).resolve().parent / "logic" / "B06_Section_Station_Swap.ts"
|
||||
CARRY_INPUT_KEYS: tuple[str, ...] = tuple(
|
||||
re.findall(
|
||||
r'"([a-z_0-9]+)"',
|
||||
re.search(
|
||||
r"export const ROW_CARRY_KEYS = \[(.*?)\] as const;",
|
||||
_SWAP_TS.read_text(encoding="utf-8"),
|
||||
re.S,
|
||||
).group(1),
|
||||
|
||||
|
||||
def _ts_keys(name: str) -> tuple[str, ...]:
|
||||
"""TS 목록 글(`export const NAME = [ "…" ] as const;`)의 칸 이름."""
|
||||
body = re.search(
|
||||
rf"export const {name} = \[(.*?)\] as const;", _SWAP_TS.read_text(encoding="utf-8"), re.S
|
||||
)
|
||||
)
|
||||
return tuple(re.findall(r'"([a-z_0-9]+)"', body.group(1)))
|
||||
|
||||
|
||||
CARRY_INPUT_KEYS: tuple[str, ...] = _ts_keys("ROW_CARRY_KEYS")
|
||||
# 자리 칸(요구 275-3 · 암 경계 · 2단 경사 · 손 상단측) — 노선이 바뀌면 같은 자리 측점만 넘김 ·
|
||||
# 정본 = TS `SEAT_KEYS`(같은 파일).
|
||||
SEAT_KEYS: tuple[str, ...] = _ts_keys("SEAT_KEYS")
|
||||
|
||||
|
||||
def stored_standard_cross_section(longitudinal_row: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
@@ -580,12 +584,15 @@ def compute_default_designs(
|
||||
samples,
|
||||
design_elevation_from_longitudinal(longitudinal, chainage_m),
|
||||
ground_type=str(user.get("ground_type") or "ripping_rock"),
|
||||
section_mode=default_modes.get(
|
||||
# 손 상단측(같은 자리로 넘긴 단면 유형 · 측구 방향 · 요구 275-3) · 없으면 상단측
|
||||
section_mode=user.get("section_mode")
|
||||
or default_modes.get(
|
||||
round(chainage_m, 3),
|
||||
f"{cross['uphill_side']}_cut"
|
||||
if cross.get("uphill_side") in ("left", "right")
|
||||
else "left_cut",
|
||||
),
|
||||
ditch_side=user.get("ditch_side"),
|
||||
ditch_type=str(user.get("ditch_type") or "standard"),
|
||||
# 포장은 사용자 구간 지정만 켠다 — 경사 제안은 경고로만 남는다(2026-08-28).
|
||||
paved=paved_at(chainage_m, paved_ranges),
|
||||
|
||||
@@ -267,7 +267,11 @@ export async function swapSectionStations(
|
||||
for (const section of swap.detail.cross_sections)
|
||||
if (section.design) section.design = { ...section.design };
|
||||
// 새 목록 열쇠 = 묶음 측점이 정한 것 그대로(규칙 한 벌 · 다시 셈하지 않음)
|
||||
const nextKeys = stations.map((station) => ({ key: station.key, chainageM: station.chainageM }));
|
||||
const nextKeys = stations.map((station) => ({
|
||||
key: station.key,
|
||||
chainageM: station.chainageM,
|
||||
seat: { x: station.x, y: station.y, left: station.left }, // 자리 칸 넘김 판정(요구 275-3)
|
||||
}));
|
||||
if (swap.moved) carryStationKeys(projectId, routeId, nextKeys);
|
||||
else useStationKeyList(projectId, routeId, nextKeys);
|
||||
// 메모리만 — 세션 상세 = 저장 정본만(387-7 · 새로고침 · B06 바로 열기 = 묶음이 다시 갈아끼움 · 365-1)
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
* 장부 · 구조물 id 는 측점 계획(초안) · 구조물 목록(횡단 보기)에서.
|
||||
* · 칸마다 열쇠 → 쓴 때 누가거리 `@at` 을 함께 담음 — 구조물 id 를 아직 모를 때(첫 열기)도 그 자리 측점이면 읽음.
|
||||
* · 옛 초안(누가거리 열쇠 `"85.05"`) · `at:` 열쇠는 읽을 때 지금 측점 열쇠로 옮겨 다시 씀.
|
||||
* · 노선이 바뀜 = `carryStationDrafts` — 넘기는 칸 = 서버 `CARRY_INPUT_KEYS` 일곱(지반 유형 · 측구 종류 ·
|
||||
* 측구 선택 · 2단 경사 · 암 경계 · 암 경사 · 소단 구간은 구간 그대로) + 사면 쉬프트 · 소단 깊이(417 · 서버
|
||||
* `ROUTE_CARRY_KEYS`) · 단면 유형 · 측구 방향은 새 지형이 정함 · 사라진 측점의 고침은 버리고 「노선 밖 고침 N 건」.
|
||||
* · 노선이 바뀜 = `carryStationDrafts` — 넘기는 칸 = 지반 유형 · 측구 종류 · 측구 선택 · 사면 쉬프트 · 소단 깊이
|
||||
* (417 · 서버 `CARRY_INPUT_KEYS` · `ROUTE_CARRY_KEYS`) · 자리 칸(암 경계 · 암 경사 · 2단 경사 · 손 상단측)은 같은
|
||||
* 자리 측점만(값마다 쓴 때 자리 `@seat` · 요구 275-3) · 사라진 측점의 고침은 버리고 「노선 밖 고침 N 건」.
|
||||
* · 노선 저장 = `carryChainageStates` — 누가거리 열쇠 칸(옹벽 · 세월 · BOX · 집수 조절 · 측점 폭 · 메모 · 상세 표 ·
|
||||
* `ROUTE_CARRY_STATES`)도 측점 열쇠로 새 노선 측점에 옮김(구조물 조절 = 관 · 구조물 열쇠만 · 서버와 같음).
|
||||
* ========================================================================== */
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
nearestKeyed,
|
||||
rawEntries,
|
||||
ROUTE_CARRY_STATES,
|
||||
SEAT_HINT,
|
||||
stationKeysFrom,
|
||||
type CarryFacilities,
|
||||
type Keyed,
|
||||
@@ -221,27 +222,31 @@ export function writeKeyed<V>(
|
||||
only?: ReadonlyArray<string>,
|
||||
): void {
|
||||
const raw = readRaw(name, projectId, routeId);
|
||||
const next: Raw = { [HINT]: {} };
|
||||
const next: Raw = { [HINT]: {}, [SEAT_HINT]: {} };
|
||||
const replaced = new Set((only ?? [...byChainage.keys()]).map((at) => chainageKey(Number(at))));
|
||||
for (const [key, value, hint] of rawEntries<unknown>(raw)) {
|
||||
for (const [key, value, hint, seat] of rawEntries<unknown>(raw)) {
|
||||
const at = chainageOfKey(projectId, routeId, key, hint);
|
||||
if (at !== null && (only === undefined || replaced.has(chainageKey(at)))) continue;
|
||||
next[key] = value;
|
||||
if (hint !== undefined) next[HINT]![key] = hint;
|
||||
if (seat) next[SEAT_HINT]![key] = seat;
|
||||
}
|
||||
const list = lists.get(scopeOf(projectId, routeId));
|
||||
for (const [at, value] of byChainage) {
|
||||
if (only !== undefined && !replaced.has(chainageKey(Number(at)))) continue;
|
||||
const key = keyAt(projectId, routeId, Number(at));
|
||||
next[key] = value;
|
||||
next[HINT]![key] = Number(chainageKey(Number(at)));
|
||||
// 쓴 때 자리 — 노선이 바뀌면 자리 칸은 이 자리 그대로인 측점만 따라감(요구 275-3)
|
||||
const seat = list && nearestKeyed(list, Number(at))?.seat;
|
||||
if (seat) next[SEAT_HINT]![key] = seat;
|
||||
}
|
||||
writeState(name, next, projectId, routeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 노선이 바뀜(새 측점 목록을 `useStationKeys` 로 세운 뒤) — 옛 노선 고침을 측점 열쇠로 새 노선에 넘김.
|
||||
* 넘김 = 카드 선택 넷(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사) · 암 경계 · 암 경사 · 단면 유형 · 측구
|
||||
* 방향은 새 지형이 정함 · 노견 확장 · 소단 깊이는 안 넘김. 돌려주는 값 = 노선 밖으로 나간 고침 수(측점 단위).
|
||||
* 넘김 규칙 = `carryKeyedStates`(자리 칸은 같은 자리만 · 요구 275-3). 돌려주는 값 = 노선 밖으로 나간 고침 수(측점 단위).
|
||||
* `quiet` = 알림 없이 수만 돌려줌(부르는 쪽이 결과 한 줄에 합칠 때).
|
||||
*/
|
||||
export function carryStationDrafts(
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*
|
||||
* · 측점 열쇠 목록 = `stationKeysFrom`(일반 grid:k · 관 pipe:<id> · 구조물 structure:<id> · 시종점 end:BP/EP ·
|
||||
* id 모름 at:<0.01 m> · 규칙 = `common_util_station_key`).
|
||||
* · 측점 열쇠 칸 다섯 = `carryKeyedStates` — 넘김 = 카드 선택 넷 · 암 경계 · 암 경사 · 사면 쉬프트 · 소단 깊이 ·
|
||||
* 단면 유형 · 측구 방향은 새 지형이 정함 · 사라진 측점 고침은 버림(수만 셈).
|
||||
* · 측점 열쇠 칸 다섯 = `carryKeyedStates` — 넘김 = 카드 선택(지반 유형 · 측구 종류 · 측구 선택) · 사면 쉬프트 ·
|
||||
* 소단 깊이 · 자리 칸(암 경계 · 암 경사 · 2단 경사 · 단면 유형 · 측구 방향 · `SEAT_KEYS`)은 같은 자리
|
||||
* (`sameSeat` · 열쇠마다 옛 · 새 자리)만 · 새 자리 = 기본값(요구 275-3) · 사라진 측점 고침은 버림(수만 셈).
|
||||
* · 누가거리 열쇠 칸(`ROUTE_CARRY_STATES`) = `carryChainageStates` — 옛 누가거리 → 옛 측점 열쇠 → 새 누가거리 ·
|
||||
* 구조물 조절 = 관 · 구조물 열쇠만.
|
||||
* 쓰는 곳 = 브라우저 세션(`B06_Section_Station_Keys` · 끌기 동안 같은 노선 안 목록 갈림) · 서버 Node 입구
|
||||
@@ -18,8 +19,10 @@ import {
|
||||
curveMoves,
|
||||
dedupeStationKeys,
|
||||
looseChainage,
|
||||
sameSeat,
|
||||
stationKey,
|
||||
type StationKeyRole,
|
||||
type StationSeat,
|
||||
} from "../../common_util/common_util_station_key";
|
||||
import type { CrossSection } from "../B06_Section_Api_Fetch";
|
||||
|
||||
@@ -33,10 +36,14 @@ export const KEYED_STATES: readonly KeyedState[] = [
|
||||
"cutslope",
|
||||
];
|
||||
|
||||
/** 노선이 바뀔 때 넘기는 카드 선택 — 서버 `CARRY_INPUT_KEYS` 중 선택 칸(나머지 셋 = rockb · cutslope · 소단 구간). */
|
||||
const CARRY_CHOICE_KEYS = ["ground_type", "ditch_type", "ditch_choice", "two_stage_slope"] as const;
|
||||
/** 노선이 바뀔 때 넘기는 카드 선택 — 서버 `CARRY_INPUT_KEYS` 중 선택 칸(나머지 = rockb · cutslope · 소단 구간). */
|
||||
const CARRY_CHOICE_KEYS = ["ground_type", "ditch_type", "ditch_choice"] as const;
|
||||
/** 카드 선택 중 자리 칸(`SEAT_KEYS` · 같은 자리만 · 요구 275-3) — 2단 경사 · 손 상단측(단면 유형 · 측구 방향). */
|
||||
const SEAT_CHOICE_KEYS = ["two_stage_slope", "section_mode", "ditch_side"] as const;
|
||||
/** 노선이 바뀔 때 통째로 넘기는 칸 — 사면 쉬프트(노견 확장) · 소단 깊이도(417 · 서버 `ROUTE_CARRY_KEYS`). */
|
||||
const CARRY_WHOLE: readonly KeyedState[] = ["rockb", "cutslope", "shoulderext", "bermdepth"];
|
||||
const CARRY_WHOLE: readonly KeyedState[] = ["shoulderext", "bermdepth"];
|
||||
/** 자리 칸 통째(같은 자리만) — 암 경계 · 암 절토 경사. */
|
||||
const SEAT_WHOLE: readonly KeyedState[] = ["rockb", "cutslope"];
|
||||
|
||||
/**
|
||||
* 노선 저장 때 새 노선 측점으로 옮기는 **누가거리 열쇠** 칸(측점 옮김 목록 · 417) → 담긴 설계 칸.
|
||||
@@ -75,9 +82,15 @@ const STRUCTURE_STATES = new Set<string>([
|
||||
/** 같은 측점으로 보는 거리 — 옛 초안 열쇠(0.01 m) · 서버 측점(0.001 m) 차이를 받음. */
|
||||
export const NEAR_M = 0.01;
|
||||
export const HINT = "@at";
|
||||
/** 칸 원본에 값마다 쓴 때 자리(`StationSeat`)를 담는 칸 — 되돌리기로 되살린 값도 제 자리로 판정(요구 275-3). */
|
||||
export const SEAT_HINT = "@seat";
|
||||
|
||||
export type Keyed = { key: string; chainageM: number };
|
||||
export type Raw = Record<string, unknown> & { [HINT]?: Record<string, number> };
|
||||
/** 목록 측점 하나 — 자리(`seat`)를 모르면 같은 자리로 봄(`sameSeat`). */
|
||||
export type Keyed = { key: string; chainageM: number; seat?: StationSeat };
|
||||
export type Raw = Record<string, unknown> & {
|
||||
[HINT]?: Record<string, number>;
|
||||
[SEAT_HINT]?: Record<string, StationSeat>;
|
||||
};
|
||||
/** 측점을 덮는 관 · 구조물(측점 계획 · 구조물 목록). */
|
||||
export type Owner = { id: string; kind: "pipe" | "structure"; at: number; role: StationKeyRole };
|
||||
/** 관 측점의 관 id — 관 장부(세션 `pipe-sN`) · 찾기만(`near`) · 없으면 지음(`at`). */
|
||||
@@ -115,7 +128,19 @@ export function stationKeysFrom(
|
||||
: stationKey("irregular", at, intervalM);
|
||||
});
|
||||
const chainages = sections.map((section) => section.chainage_m);
|
||||
return dedupeStationKeys(keys, chainages).map((key, i) => ({ key, chainageM: chainages[i] }));
|
||||
return dedupeStationKeys(keys, chainages).map((key, i) => ({
|
||||
key,
|
||||
chainageM: chainages[i],
|
||||
seat: seatOf(sections[i]),
|
||||
}));
|
||||
}
|
||||
|
||||
/** 횡단 측점 자리(중심 · 왼쪽 축) — 좌표가 없으면 undefined. */
|
||||
export function seatOf(section: CrossSection): StationSeat | undefined {
|
||||
const { center_x: x, center_y: y } = section;
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined;
|
||||
const left = section.frame?.left_xy;
|
||||
return { x, y, left: left ? [left[0], left[1]] : null };
|
||||
}
|
||||
|
||||
/** 목록에서 그 자리(0.01 m 안) 가장 가까운 측점. */
|
||||
@@ -161,12 +186,15 @@ export function curveMovesIn(
|
||||
return curveMoves(pairs(from), pairs(to));
|
||||
}
|
||||
|
||||
/** 칸 원본 → [열쇠, 값, 쓴 때 자리]. */
|
||||
export function rawEntries<V>(raw: Raw): Array<[string, V, number | undefined]> {
|
||||
/** 칸 원본 → [열쇠, 값, 쓴 때 누가거리, 쓴 때 자리]. */
|
||||
export function rawEntries<V>(
|
||||
raw: Raw,
|
||||
): Array<[string, V, number | undefined, StationSeat | undefined]> {
|
||||
const hints = raw[HINT] ?? {};
|
||||
const seats = raw[SEAT_HINT] ?? {};
|
||||
return Object.entries(raw)
|
||||
.filter(([key]) => key !== HINT)
|
||||
.map(([key, value]) => [key, value as V, hints[key]]);
|
||||
.filter(([key]) => key !== HINT && key !== SEAT_HINT)
|
||||
.map(([key, value]) => [key, value as V, hints[key], seats[key]]);
|
||||
}
|
||||
|
||||
/** 칸 원본을 목록 누가거리(0.01 m 글) → 값으로 읽음 — 지금 측점에 없는 고침은 뺌. */
|
||||
@@ -179,13 +207,14 @@ export function readKeyedIn<V>(raw: Raw, list: ReadonlyArray<Keyed> | undefined)
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 칸마다 넘기는 값 — 넘길 것이 없으면 null. */
|
||||
function carriedValue(name: KeyedState, value: unknown): unknown {
|
||||
if (CARRY_WHOLE.includes(name)) return value;
|
||||
/** 칸마다 넘기는 값 — 넘길 것이 없으면 null · `seated` = 같은 자리(자리 칸도 넘김 · 요구 275-3). */
|
||||
function carriedValue(name: KeyedState, value: unknown, seated: boolean): unknown {
|
||||
if (CARRY_WHOLE.includes(name) || (seated && SEAT_WHOLE.includes(name))) return value;
|
||||
if (name !== "crossdesign" || !value || typeof value !== "object") return null;
|
||||
const source = value as Record<string, unknown>;
|
||||
const keys = seated ? [...CARRY_CHOICE_KEYS, ...SEAT_CHOICE_KEYS] : CARRY_CHOICE_KEYS;
|
||||
const picked = Object.fromEntries(
|
||||
CARRY_CHOICE_KEYS.filter((key) => source[key] !== undefined).map((key) => [key, source[key]]),
|
||||
keys.filter((key) => source[key] !== undefined).map((key) => [key, source[key]]),
|
||||
);
|
||||
return Object.keys(picked).length ? picked : null;
|
||||
}
|
||||
@@ -203,22 +232,26 @@ export function carryKeyedStates(
|
||||
const curves = curveMovesIn(from, to);
|
||||
const states = {} as Record<KeyedState, Raw>;
|
||||
for (const name of KEYED_STATES) {
|
||||
const next: Raw = { [HINT]: {} };
|
||||
for (const [key, value] of rawEntries<unknown>(read(name))) {
|
||||
const next: Raw = { [HINT]: {}, [SEAT_HINT]: {} };
|
||||
for (const [key, value, , wroteAt] of rawEntries<unknown>(read(name))) {
|
||||
// 옛 열쇠(누가거리 · at:)는 옛 목록으로 측점 열쇠부터 · 측점 열쇠(id)는 그대로
|
||||
const fromAt = looseChainage(key) === null ? null : chainageIn(from, key);
|
||||
const was = fromAt === null ? key : keyIn(from, fromAt);
|
||||
const stable = curves.get(was) ?? was;
|
||||
const toAt = chainageIn(to, stable, looseChainage(stable) ?? undefined);
|
||||
const carried = carriedValue(name, value);
|
||||
if (toAt === null || stable.startsWith("at:")) {
|
||||
if (carried !== null) outside.add(stable);
|
||||
if (carriedValue(name, value, true) !== null) outside.add(stable);
|
||||
continue;
|
||||
}
|
||||
if (carried === null) continue;
|
||||
const toKey = keyIn(to, toAt);
|
||||
// 값의 자리 = 쓴 때 자리(되돌리기로 되살린 값도) → 없으면 옛 목록 그 측점 자리
|
||||
const seat = wroteAt ?? from?.find((entry) => entry.key === was)?.seat;
|
||||
const seated = sameSeat(seat, to && nearestKeyed(to, toAt)?.seat);
|
||||
const carried = carriedValue(name, value, seated);
|
||||
if (carried === null) continue;
|
||||
next[toKey] = carried;
|
||||
next[HINT]![toKey] = toAt;
|
||||
if (seat) next[SEAT_HINT]![toKey] = seat;
|
||||
}
|
||||
states[name] = next;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
* 안 덮음 · 467 · 요구 272) · 설계는 지금 규칙.
|
||||
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
|
||||
* · 상단측 = 새 지반 표본으로 다시 셈한 측점은 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
|
||||
* · 노선 초안 차례 · 자리가 바뀐 측점(`sameSeat` 아님) = 앞 설계의 자리 칸(`SEAT_KEYS` · 암 경계 · 2단 경사 · 손
|
||||
* 상단측)을 걷고 기본값(단면 유형 = 새 상단측 깎기 · 요구 275-3 · 서버 노선 저장 · 넘김과 같은 규칙).
|
||||
* · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자 칸(만진 칸 · 지반 등 입력 `ROW_CARRY_KEYS`) — 서버가
|
||||
* 그 행을 지우고 기본 설계에 [저장] patch 를 얹음(`sync_cross_section_rows` · `compute_default_designs` · 428) ·
|
||||
* patch 가 그 칸을 모두 실음(`crossPatchesFromCache` · 445 H2).
|
||||
@@ -27,7 +29,7 @@
|
||||
import type { PipelineStation } from "../../A00_Common/design/logic/design_pipeline_types";
|
||||
import type { CrossSection, SectionDetailResponse, SectionStation } from "../B06_Section_Api_Fetch";
|
||||
import { uphillSide } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import { curveMoves } from "../../common_util/common_util_station_key";
|
||||
import { curveMoves, sameSeat } from "../../common_util/common_util_station_key";
|
||||
|
||||
interface Sample {
|
||||
offset_m: number;
|
||||
@@ -92,6 +94,19 @@ export const ROW_CARRY_KEYS = [
|
||||
"berm",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 자리 칸(요구 275-3) — 그 자리 지형에 묶인 사용자 값(암 경계 · 2단 경사 · 손 상단측 = 단면 유형 · 측구 방향).
|
||||
* 노선이 바뀌면 같은 자리(`sameSeat`) 측점만 들고 감 · 새 자리 = 기본값 · 목록 정본 = 여기 하나(서버
|
||||
* `SEAT_KEYS` 가 이 글을 읽음 · 선언 꼴 그대로 둠).
|
||||
*/
|
||||
export const SEAT_KEYS = [
|
||||
"two_stage_slope",
|
||||
"rock_boundary_offset_m",
|
||||
"cut_slope_ratio_user",
|
||||
"section_mode",
|
||||
"ditch_side",
|
||||
] as const;
|
||||
|
||||
/** 옮긴 시설 측점 설계 — 기본 설계 위에 앞 설계의 나머지(사용자 칸 · 지반 등 입력) · 결과 칸은 다시 셈이 갈음. */
|
||||
function reseeded(
|
||||
prior: CrossSection["design"],
|
||||
@@ -104,6 +119,17 @@ function reseeded(
|
||||
return design as unknown as NonNullable<CrossSection["design"]>;
|
||||
}
|
||||
|
||||
/** 자리가 바뀐 측점 설계 — 자리 칸을 걷고 기본값(암 경계 = config · 2단 경사 끔 · 단면 유형 = 상단측 깎기). */
|
||||
function unseated(
|
||||
prior: CrossSection["design"],
|
||||
uphill: CrossSection["uphill_side"],
|
||||
): NonNullable<CrossSection["design"]> {
|
||||
const design = { ...(prior ?? {}) } as Record<string, unknown>;
|
||||
for (const key of SEAT_KEYS) delete design[key];
|
||||
design.section_mode = freshDesign(uphill).section_mode;
|
||||
return design as unknown as NonNullable<CrossSection["design"]>;
|
||||
}
|
||||
|
||||
function kindOf(station: PipelineStation): SectionStation["kind"] {
|
||||
if (station.kind === "end") return station.key.endsWith("EP") ? "ep" : "bp";
|
||||
if (station.kind === "curve") return "curve";
|
||||
@@ -316,12 +342,27 @@ export function swapCrossSections(
|
||||
const reseed =
|
||||
(station.kind === "pipe" || station.kind === "structure") &&
|
||||
Math.abs(prior.chainage_m - station.chainageM) > FACILITY_RESEED_M;
|
||||
// 노선 초안 차례에 자리가 바뀜 = 자리 칸 기본값(요구 275-3)
|
||||
const unseat =
|
||||
routeDraft &&
|
||||
!sameSeat(
|
||||
{ x: prior.center_x, y: prior.center_y, left: prior.frame?.left_xy },
|
||||
{ x: station.x, y: station.y, left: station.left },
|
||||
);
|
||||
const next: CrossSection = {
|
||||
...prior,
|
||||
...place,
|
||||
uphill_side: uphill,
|
||||
samples,
|
||||
...(reseed ? { design: reseeded(prior.design, uphill) } : {}),
|
||||
...(reseed
|
||||
? {
|
||||
design: unseat
|
||||
? unseated(reseeded(prior.design, uphill), uphill)
|
||||
: reseeded(prior.design, uphill),
|
||||
}
|
||||
: unseat && prior.design
|
||||
? { design: unseated(prior.design, uphill) }
|
||||
: {}),
|
||||
};
|
||||
if (!onGrid) delete next.grid; // 격자 자리를 떠난 시설 측점 — 옛 숨은 일반 측점 표시를 안 들고 감(458)
|
||||
// 묶음 측점 = 정본 목록 — 초안 맞춤의 「구조물 없는 사본」 표시를 안 들고 감(들고 가면 다음 맞춤이 그 측점을 버림)
|
||||
|
||||
@@ -107,3 +107,35 @@ def curve_moves(before: list[tuple[str, float]], after: list[tuple[str, float]])
|
||||
if best >= 0:
|
||||
out[key] = free.pop(best)[0]
|
||||
return out
|
||||
|
||||
|
||||
#: 같은 자리로 보는 중심 차(m) · 축 차 — 브라우저 틀 ↔ 서버 틀 차(467)를 넘김.
|
||||
SEAT_XY_M = 0.01
|
||||
SEAT_AXIS = 1e-3
|
||||
|
||||
|
||||
def station_seat(station: dict | None) -> tuple[float, float, tuple[float, float] | None] | None:
|
||||
"""종단 측점 줄(``center_x`` · ``center_y`` · ``frame.left_xy``) → 자리 · 모르면 None."""
|
||||
if not isinstance(station, dict):
|
||||
return None
|
||||
try:
|
||||
x, y = float(station["center_x"]), float(station["center_y"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return None
|
||||
left = (station.get("frame") or {}).get("left_xy")
|
||||
axis = (float(left[0]), float(left[1])) if isinstance(left, list) and len(left) == 2 else None
|
||||
return x, y, axis
|
||||
|
||||
|
||||
def same_seat(a, b) -> bool:
|
||||
"""노선이 바뀐 뒤 그 측점이 같은 자리인가(요구 275-3) — 짝 ``sameSeat`` · 거울 475-3.
|
||||
|
||||
자리 = (x, y, 왼쪽 단위 벡터 | None) · 한쪽을 모르면 같은 자리로 봄(옛 동작).
|
||||
"""
|
||||
if a is None or b is None:
|
||||
return True
|
||||
if math.hypot(a[0] - b[0], a[1] - b[1]) > SEAT_XY_M:
|
||||
return False
|
||||
if a[2] is None or b[2] is None:
|
||||
return True
|
||||
return abs(a[2][0] - b[2][0]) <= SEAT_AXIS and abs(a[2][1] - b[2][1]) <= SEAT_AXIS
|
||||
|
||||
@@ -97,3 +97,30 @@ export function curveMoves(
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 측점 자리 — 중심 좌표 · 왼쪽 단위 벡터(횡단 축 · 없으면 좌표만 봄). */
|
||||
export interface StationSeat {
|
||||
x: number;
|
||||
y: number;
|
||||
left?: readonly [number, number] | null;
|
||||
}
|
||||
/** 같은 자리로 보는 중심 차(m) · 축 차 — 브라우저 틀 ↔ 서버 틀 차(467)를 넘김. */
|
||||
export const SEAT_XY_M = 0.01;
|
||||
export const SEAT_AXIS = 1e-3;
|
||||
|
||||
/**
|
||||
* 노선이 바뀐 뒤 그 측점이 같은 자리인가(요구 275-3) — 자리 · 축이 같으면 지형이 같아 「자리 칸」(암 경계 · 2단
|
||||
* 경사 · 손 상단측)을 들고 감 · 한쪽 자리를 모르면 같은 자리로 봄(옛 목록 · 옛 동작) · 짝 `same_seat` ·
|
||||
* 거울 시험 `test_475_3_seat_carry.py`.
|
||||
*/
|
||||
export function sameSeat(
|
||||
a: StationSeat | null | undefined,
|
||||
b: StationSeat | null | undefined,
|
||||
): boolean {
|
||||
if (!a || !b) return true;
|
||||
if (Math.hypot(a.x - b.x, a.y - b.y) > SEAT_XY_M) return false;
|
||||
if (!a.left || !b.left) return true;
|
||||
return (
|
||||
Math.abs(a.left[0] - b.left[0]) <= SEAT_AXIS && Math.abs(a.left[1] - b.left[1]) <= SEAT_AXIS
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
|
||||
| [저장] · [확정] = 서버가 입력만으로 정본 다시 셈 · 브라우저 결과(면적 · 표본 · 측점) 받아 적지 않음 | `design_save.ts:58-68` · `B06_Section_Router_Confirm.py:203-215` | `test_b06_save_recompute` |
|
||||
| 자동저장(디바운스 PUT) 없음 · 화면 잠금 = [저장] · [확정] · [초기화] · 추천 [적용]만 | `design_save.ts:249` | — |
|
||||
| 측점 열쇠 = `common_util_station_key` 한 벌 · 노선 바뀜 = 관 · 구조물 좌표(x · y) 따라감 · 누가거리 다시 매김 | `common_util_station_key.ts:32` · `common_util_route_follow.ts:291` | `test_344_3_station_key_mirror` · `test_410_follow_xy` |
|
||||
| 노선 바뀜 넘김 = 열쇠마다 지반 · 측구 종류 · 측구 선택 · 사면 쉬프트 · 소단 · 소단 깊이 · 자리 칸(`SEAT_KEYS` = 암 경계 · 암 절토 경사 · 2단 경사 · 손 상단측 = 단면 유형 · 측구 방향)은 같은 자리(`sameSeat` · 중심 0.01 m · 축 1e-3) 측점만 · 새 자리 = 기본값 · 세션 값은 쓴 때 자리(`@seat`)로 판정(↶ ↷ 유지) · 끌기 갈아끼움 · 미리보기 · Node 넘김 · 서버 노선 저장 한 규칙 | `logic/B06_Section_Station_Swap.ts` SEAT_KEYS · unseated · `logic/B06_Section_Route_Carry.ts` carryKeyedStates · `B05_Profile/logic/B05_Profile_Pipeline_Detail.ts` crossAtSeat · `B05_Profile_Route_Save_Parts.py` carry_cross_inputs | `test_475_3_seat_carry` |
|
||||
| 노선 초안 ↶(버림) = 차례 하나 · 버림 동안 종단도 그리기 멈춤 · 낡음 다시 셈 미룸 → 되돌림 · 저장 기준 맞춤 · 뒤 차례 끝에 한 번 그림(사이 섞인 유토곡선 없음) | `design_pipeline_page.ts:642 · 403` · `B06_Section_Section_Store.ts:316` | `test_472_5c_drop_once` |
|
||||
| B05 · B06 캐시 = 한 벌 · [저장] 한 번에 함께 나감 | `design_shared.ts:395-448` · `design_save.ts:237` | — |
|
||||
| 관 · 구조물 주인 = 가게 칸 하나 — 화면 사본은 가게 알림으로 맞춤(제 쓰기는 같은 값 · 남 고침은 다시 실음) | `design_view_profile.ts` 가게 구독 · `design_view_cross.ts` onShop · `B06_Section_UI_Page_Structures_Panel.ts` seen | `test_472_5_shop_owner` |
|
||||
|
||||
@@ -38,7 +38,7 @@ const R2 = 8;
|
||||
const raw = (name, route) => m.readState(name, P, route) ?? {};
|
||||
const keysOf = (name, route) =>
|
||||
Object.keys(raw(name, route))
|
||||
.filter((k) => k !== "@at")
|
||||
.filter((k) => !k.startsWith("@")) // 「@at」 · 「@seat」 = 쓴 때 자리 칸(475-3)
|
||||
.sort();
|
||||
const section = (chainage_m, kind, extra = {}) => ({
|
||||
station_id: `s${chainage_m}`,
|
||||
|
||||
@@ -173,7 +173,7 @@ const NEW = (dy) => [
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([at, c]) => [at, c.ground_type, c.section_mode ?? null]),
|
||||
rawKeys: Object.keys(m.readState("crossdesign", P, R) ?? {})
|
||||
.filter((k) => k !== "@at")
|
||||
.filter((k) => !k.startsWith("@"))
|
||||
.sort(),
|
||||
widths: m.readState("crossw", P, R),
|
||||
redraws,
|
||||
|
||||
@@ -97,10 +97,10 @@ const snap = () => ({
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([at, c]) => [at, c.ground_type ?? null, c.section_mode ?? null]),
|
||||
shoulder: Object.entries(m.readState("shoulderext", P, R) ?? {})
|
||||
.filter(([k]) => k !== "@at")
|
||||
.filter(([k]) => !k.startsWith("@"))
|
||||
.map(([k, v]) => [k, v]),
|
||||
berm: Object.entries(m.readState("bermdepth", P, R) ?? {})
|
||||
.filter(([k]) => k !== "@at")
|
||||
.filter(([k]) => !k.startsWith("@"))
|
||||
.map(([k, v]) => [k, v]),
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const R2 = 8;
|
||||
const raw = (name, route) => m.readState(name, P, route) ?? {};
|
||||
const keysOf = (name, route) =>
|
||||
Object.keys(raw(name, route))
|
||||
.filter((k) => k !== "@at")
|
||||
.filter((k) => !k.startsWith("@")) // 「@at」 · 「@seat」 = 쓴 때 자리 칸(475-3)
|
||||
.sort();
|
||||
const section = (chainage_m, kind, extra = {}) => ({
|
||||
station_id: `s${chainage_m}`,
|
||||
|
||||
@@ -6,7 +6,7 @@ const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
const idOf = (m) => `station_${String(Math.round(m * 1000)).padStart(12, "0")}`;
|
||||
const REAL =
|
||||
/(B05_Profile_Pipeline_Detail|B06_Section_Station_Swap|design_pipeline_cross|common_util_\w+)$/;
|
||||
/(B05_Profile_Pipeline_Detail|B06_Section_Station_Swap|B06_Section_Route_Carry|design_pipeline_cross|common_util_\w+)$/; // Route_Carry = 자리 칸 「@seat」(475-3)
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
const STUBS = {
|
||||
B05_Profile_Pipeline_Grade: { stationIdOf: idOf },
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* 475-3 시험 묶음 진입점 — 자리 칸(암 경계 · 2단 경사 · 손 상단측) 넘김 규칙을 화면 · 서버 없이 잰다. */
|
||||
export { sameSeat } from "../../common_util/common_util_station_key";
|
||||
export { carryKeyedStates } from "../../B06_Section/logic/B06_Section_Route_Carry";
|
||||
export { crossAtSeat } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
export { swapCrossSections } from "../../B06_Section/logic/B06_Section_Station_Swap";
|
||||
@@ -0,0 +1,135 @@
|
||||
/* 475-3 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_475_3_entry.ts)을 부르고 결과를 JSON 한 벌로.
|
||||
* node helper_475_3_seat_carry.mjs <묶음.mjs> <입력.json>
|
||||
* 입력 = { seats: [[a, b], …] }(자리 판정 거울 사례).
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const [bundle, inputPath] = process.argv.slice(2);
|
||||
const m = await import(pathToFileURL(bundle).href);
|
||||
const input = JSON.parse(readFileSync(inputPath, "utf-8"));
|
||||
const out = {};
|
||||
|
||||
out.sameSeat = input.seats.map(([a, b]) => m.sameSeat(a, b));
|
||||
|
||||
/* 노선 바뀜 넘김 — grid:1 같은 자리 · grid:2 옮김 · grid:3 옮겼지만 값은 새 자리에서 씀(되돌리기로 되살린 값) */
|
||||
const A = { x: 0, y: 20, left: [-1, 0] };
|
||||
const B = { x: 0, y: 40, left: [-1, 0] };
|
||||
const C = { x: 0, y: 60, left: [-1, 0] };
|
||||
const C2 = { x: 3, y: 60, left: [-1, 0] };
|
||||
const from = [
|
||||
{ key: "grid:1", chainageM: 20, seat: A },
|
||||
{ key: "grid:2", chainageM: 40, seat: B },
|
||||
{ key: "grid:3", chainageM: 60, seat: C },
|
||||
];
|
||||
const to = [
|
||||
{ key: "grid:1", chainageM: 20, seat: A },
|
||||
{ key: "grid:2", chainageM: 40.5, seat: { x: 5, y: 40, left: [-1, 0] } },
|
||||
{ key: "grid:3", chainageM: 61, seat: C2 },
|
||||
];
|
||||
const choice = {
|
||||
ground_type: "soil",
|
||||
ditch_type: "l_type",
|
||||
two_stage_slope: true,
|
||||
section_mode: "right_cut",
|
||||
ditch_side: "left",
|
||||
paved: true,
|
||||
};
|
||||
const raw = {
|
||||
rockb: { "grid:1": 1.2, "grid:2": 1.5, "grid:3": 0.9, "@seat": { "grid:3": C2 } },
|
||||
cutslope: { "grid:1": 0.8, "grid:2": 0.7 },
|
||||
crossdesign: { "grid:1": choice, "grid:2": choice },
|
||||
shoulderext: { "grid:2": { left: 0.5 } },
|
||||
bermdepth: {},
|
||||
};
|
||||
const strip = (states) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(states).map(([name, cells]) => [
|
||||
name,
|
||||
Object.fromEntries(Object.entries(cells).filter(([key]) => !key.startsWith("@"))),
|
||||
]),
|
||||
);
|
||||
const carried = m.carryKeyedStates((name) => raw[name] ?? {}, from, to);
|
||||
out.carried = strip(carried.states);
|
||||
out.carriedSeats = carried.states.rockb["@seat"];
|
||||
out.outside = carried.outside.size;
|
||||
/* 자리 모름(옛 목록) = 옛 동작 그대로 */
|
||||
const bare = (list) => list.map(({ key, chainageM }) => ({ key, chainageM }));
|
||||
out.unknown = strip(m.carryKeyedStates((name) => raw[name] ?? {}, bare(from), bare(to)).states);
|
||||
|
||||
/* 미리보기 입력 — 저장분 · 세션 자리 칸 묶음(`seats`) */
|
||||
const station = (key, x, y) => ({ key, chainageM: y, x, y, left: [-1, 0] });
|
||||
const input2 = {
|
||||
standard: {},
|
||||
byKey: {
|
||||
"grid:1": {
|
||||
groundType: "soil",
|
||||
rockBoundaryOffsetM: 1,
|
||||
sectionMode: "right_cut",
|
||||
seats: [{ ...A, fields: ["rockBoundaryOffsetM", "sectionMode"] }],
|
||||
},
|
||||
"grid:2": {
|
||||
groundType: "soil",
|
||||
twoStageSlope: true,
|
||||
cutSlopeRatio: 0.7,
|
||||
seats: [
|
||||
{ ...B, fields: ["twoStageSlope"] },
|
||||
{ x: 5, y: 40, left: [-1, 0], fields: ["cutSlopeRatio"] },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
out.atSeat = m.crossAtSeat(input2, [station("grid:1", 0, 20), station("grid:2", 5, 40)]).byKey;
|
||||
|
||||
/* B06 갈아끼움(노선 초안) — 앞 횡단 설계를 이어 받되 자리가 바뀐 측점은 자리 칸 기본값 */
|
||||
const hand = {
|
||||
ground_type: "soil",
|
||||
section_mode: "both_cut",
|
||||
ditch_side: "right",
|
||||
two_stage_slope: true,
|
||||
rock_boundary_offset_m: -1.2,
|
||||
cut_slope_ratio_user: 0.8,
|
||||
shoulder_extend_left_m: 0.5,
|
||||
};
|
||||
const section = (chainage, x) => ({
|
||||
station_id: `s${chainage}`,
|
||||
chainage_m: chainage,
|
||||
kind: "regular",
|
||||
center_x: x,
|
||||
center_y: chainage,
|
||||
frame: { left_xy: [-1, 0] },
|
||||
samples: [],
|
||||
uphill_side: "left",
|
||||
design: { ...hand },
|
||||
});
|
||||
const pipelineStation = (key, chainage, x) => ({
|
||||
key,
|
||||
kind: "grid",
|
||||
chainageM: chainage,
|
||||
x,
|
||||
y: chainage,
|
||||
left: [-1, 0],
|
||||
azimuthDeg: 0,
|
||||
planRadiusM: null,
|
||||
curveOuterSide: null,
|
||||
curveWideningM: 0,
|
||||
ground: null,
|
||||
});
|
||||
const detail = {
|
||||
longitudinal: { stations: [], length_m: 40 },
|
||||
cross_sections: [section(20, 0), section(40, 0)],
|
||||
};
|
||||
const swapped = (draft) =>
|
||||
m
|
||||
.swapCrossSections(
|
||||
detail,
|
||||
[pipelineStation("grid:1", 20, 0), pipelineStation("grid:2", 40, 5)],
|
||||
["grid:1", "grid:2"],
|
||||
20,
|
||||
draft,
|
||||
)
|
||||
.detail.cross_sections.map((s) => s.design);
|
||||
out.swapDraft = swapped(true);
|
||||
out.swapNoDraft = swapped(false);
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -97,11 +97,15 @@ def test_노선_바뀜_측점_열쇠로_넘김(out):
|
||||
assert out["outside"] == 2 # 60 m(노선이 짧아짐) · 99 m(옛 노선에도 없던 옛 초안)
|
||||
assert out["carriedKeys"] == ["grid:1", "grid:2", "pipe:pipe-s1", "structure:st-1"]
|
||||
assert out["carriedRead"] == [20, 40, 47, 55] # 관 · 구조물은 옮긴 자리로
|
||||
# 자리 칸(2단 경사 · 손 상단측) = 같은 자리만(475-3) — 이 측점들은 좌표가 없어(자리 모름)
|
||||
# 같은 자리로 봄 · 옛 단언(단면 유형 · 측구 방향은 늘 새 지형)을 고침 · 포장은 안 넘김
|
||||
assert out["carriedChoice"] == {
|
||||
"ground_type": "soil",
|
||||
"ditch_type": "l_type",
|
||||
"ditch_choice": False,
|
||||
"two_stage_slope": True,
|
||||
"section_mode": "right_cut",
|
||||
"ditch_side": "right",
|
||||
}
|
||||
assert out["carriedRock"] == {"20.00": -0.8}
|
||||
assert out["carriedShoulder"] == ["grid:2"] # 사면 쉬프트도 넘김(417)
|
||||
|
||||
@@ -97,7 +97,9 @@ def test_넘김_값_사용자_칸(out, carried) -> None:
|
||||
by = _by_chainage(carried["patches"])
|
||||
assert by["20.00"]["ground_type"] == "soil" and by["20.00"]["two_stage_slope"] is True
|
||||
assert by["20.00"]["ditch_type"] == "u_flume"
|
||||
assert "section_mode" not in by["20.00"] # 단면 유형 = 새 지형이 정함
|
||||
# 손 단면 유형 = 자리 칸(475-3) — 이 목록은 자리를 몰라(옛 목록) 같은 자리로 봄 · 넘김
|
||||
# (옛 단언 = 늘 새 지형)
|
||||
assert by["20.00"]["section_mode"] == "left_cut"
|
||||
assert by["20.00"]["structure_note"] == "앞 메모"
|
||||
assert by["40.00"]["shoulder_extend_left_m"] == 0.5
|
||||
assert "revet_adjust" not in by["40.00"] # 일반 측점 옹벽 조절 = 안 넘김(관 · 구조물 열쇠만)
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""475-3(요구 275-3) — 노선이 바뀌면 자리 칸은 같은 자리 측점만 들고 감.
|
||||
|
||||
자리 칸 = 암 경계 · 2단 경사 · 손 상단측(TS `SEAT_KEYS` 하나 · 파이썬은 그 글을 읽음).
|
||||
|
||||
못박는 것
|
||||
- 자리 판정 거울 — TS `sameSeat` ↔ 파이썬 `same_seat`(중심 0.01 m · 왼쪽 축 1e-3 ·
|
||||
모르면 같은 자리).
|
||||
- 브라우저 · Node 넘김(`carryKeyedStates`) — 같은 자리 = 자리 칸도 · 옮긴 자리 = 자리 칸
|
||||
버림(새 자리 기본값) · 지반 · 측구 종류 · 사면 쉬프트는 그대로 · 값의 쓴 때 자리(`@seat` ·
|
||||
되돌리기로 되살린 값)가 새 자리와 같으면 유지 · 자리 모르는 옛 목록 = 옛 동작.
|
||||
- 미리보기 입력(`crossAtSeat`) — 자리 칸 묶음이 측점 자리와 다르면 뺌 · 같으면 그대로.
|
||||
- 서버 노선 저장 넘김(`carry_cross_inputs` · `seats`) — 같은 규칙 · `seats` 없으면 옛 동작.
|
||||
- 기본 설계가 넘긴 손 단면 유형으로 셈(`compute_default_designs` · 없으면 상단측 깎기).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_475_3_seat_carry.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections import find_cross_file # noqa: E402
|
||||
from B05_Profile.B05_Profile_Route_Save_Parts import carry_cross_inputs, station_seats # noqa: E402
|
||||
from B06_Section.B06_Section_Router_Design import ( # noqa: E402
|
||||
CARRY_INPUT_KEYS,
|
||||
SEAT_KEYS,
|
||||
compute_default_designs,
|
||||
)
|
||||
from common_util.common_util_station_key import same_seat, station_seat # noqa: E402
|
||||
|
||||
SEATS = [
|
||||
[{"x": 0, "y": 0, "left": [1, 0]}, {"x": 0.009, "y": 0, "left": [1, 0]}],
|
||||
[{"x": 0, "y": 0, "left": [1, 0]}, {"x": 0.011, "y": 0, "left": [1, 0]}],
|
||||
[{"x": 0, "y": 0, "left": [1, 0]}, {"x": 0, "y": 0, "left": [0.998, 0.002]}],
|
||||
[{"x": 0, "y": 0, "left": [1, 0]}, {"x": 0, "y": 0, "left": [0.9995, 0.0009]}],
|
||||
[{"x": 0, "y": 0, "left": None}, {"x": 0, "y": 0.005, "left": [0, 1]}],
|
||||
[None, {"x": 9, "y": 9, "left": [0, 1]}],
|
||||
]
|
||||
|
||||
|
||||
def _py_seat(seat):
|
||||
if seat is None:
|
||||
return None
|
||||
left = seat.get("left")
|
||||
return seat["x"], seat["y"], tuple(left) if left else None
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory):
|
||||
work = tmp_path_factory.mktemp("b475_3")
|
||||
build = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_475_3_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
)
|
||||
assert build.returncode == 0, build.stderr
|
||||
bundle = next(work.glob("*.js"), None) or next(work.glob("*.mjs"))
|
||||
inputs = work / "input.json"
|
||||
inputs.write_text(json.dumps({"seats": SEATS}), "utf-8")
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_475_3_seat_carry.mjs"), str(bundle), str(inputs)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_자리_칸_목록_정본은_TS_하나():
|
||||
swap = (ROOT / "B06_Section" / "logic" / "B06_Section_Station_Swap.ts").read_text("utf-8")
|
||||
body = re.search(r"export const SEAT_KEYS = \[(.*?)\] as const;", swap, re.S)
|
||||
assert tuple(re.findall(r'"([a-z_0-9]+)"', body.group(1))) == SEAT_KEYS
|
||||
assert SEAT_KEYS == (
|
||||
"two_stage_slope",
|
||||
"rock_boundary_offset_m",
|
||||
"cut_slope_ratio_user",
|
||||
"section_mode",
|
||||
"ditch_side",
|
||||
)
|
||||
|
||||
|
||||
def test_자리_판정_거울(out):
|
||||
assert out["sameSeat"] == [same_seat(_py_seat(a), _py_seat(b)) for a, b in SEATS]
|
||||
assert out["sameSeat"] == [True, False, False, True, True, True]
|
||||
|
||||
|
||||
def test_같은_자리는_자리_칸도_옮긴_자리는_기본값(out):
|
||||
carried = out["carried"]
|
||||
assert carried["rockb"] == {
|
||||
"grid:1": 1.2,
|
||||
"grid:3": 0.9,
|
||||
} # grid:2 옮김 = 버림 · grid:3 = 쓴 때 자리
|
||||
assert carried["cutslope"] == {"grid:1": 0.8}
|
||||
assert carried["crossdesign"]["grid:1"] == { # 포장은 안 넘김(138-1)
|
||||
"ground_type": "soil",
|
||||
"ditch_type": "l_type",
|
||||
"two_stage_slope": True,
|
||||
"section_mode": "right_cut",
|
||||
"ditch_side": "left",
|
||||
}
|
||||
assert carried["crossdesign"]["grid:2"] == {"ground_type": "soil", "ditch_type": "l_type"}
|
||||
assert carried["shoulderext"] == {"grid:2": {"left": 0.5}} # 자리 칸 아님 = 그대로
|
||||
assert out["carriedSeats"]["grid:3"] == {"x": 3, "y": 60, "left": [-1, 0]}
|
||||
assert out["outside"] == 0
|
||||
|
||||
|
||||
def test_자리_모르는_옛_목록은_옛_동작(out):
|
||||
unknown = out["unknown"]
|
||||
assert unknown["rockb"] == {"grid:1": 1.2, "grid:2": 1.5, "grid:3": 0.9}
|
||||
assert unknown["crossdesign"]["grid:2"]["two_stage_slope"] is True
|
||||
|
||||
|
||||
def test_미리보기_입력_자리_칸(out):
|
||||
at = out["atSeat"]
|
||||
assert at["grid:1"] == {
|
||||
"groundType": "soil",
|
||||
"rockBoundaryOffsetM": 1,
|
||||
"sectionMode": "right_cut",
|
||||
}
|
||||
# grid:2 = 저장 자리(0, 40)에서 온 2단 경사는 뺌 · 새 자리(5, 40)에서 쓴 암 경사는 그대로
|
||||
assert at["grid:2"] == {"groundType": "soil", "cutSlopeRatio": 0.7}
|
||||
|
||||
|
||||
def test_B06_갈아끼움_자리_바뀐_측점은_자리_칸_기본값(out):
|
||||
same, moved = out["swapDraft"]
|
||||
assert same["rock_boundary_offset_m"] == -1.2 and same["section_mode"] == "both_cut"
|
||||
assert moved == {
|
||||
"ground_type": "soil",
|
||||
"section_mode": "left_cut",
|
||||
"shoulder_extend_left_m": 0.5,
|
||||
}
|
||||
# 노선 초안 아닌 차례(관 · 구조물 · 추가 측점만) = 이어 받은 설계 그대로
|
||||
assert out["swapNoDraft"][1]["two_stage_slope"] is True
|
||||
|
||||
|
||||
def _row(chainage, x, left=(-1.0, 0.0)):
|
||||
return {
|
||||
"chainage_m": chainage,
|
||||
"center_x": x,
|
||||
"center_y": chainage,
|
||||
"frame": {"left_xy": list(left)},
|
||||
}
|
||||
|
||||
|
||||
def test_서버_노선_저장_넘김_같은_규칙():
|
||||
old_keys = {"grid:1": 20.0, "grid:2": 40.0}
|
||||
new_keys = {"grid:1": 20.0, "grid:2": 40.5}
|
||||
old_rows = [_row(20.0, 0.0), _row(40.0, 0.0)]
|
||||
new_rows = [_row(20.0, 0.0), _row(40.5, 5.0)]
|
||||
seats = (station_seats(old_rows, old_keys), station_seats(new_rows, new_keys))
|
||||
hand = {
|
||||
"ground_type": "soil",
|
||||
"two_stage_slope": True,
|
||||
"rock_boundary_offset_m": 1.5,
|
||||
"cut_slope_ratio_user": 0.8,
|
||||
"section_mode": "right_cut",
|
||||
"ditch_side": "left",
|
||||
"shoulder_extend_left_m": 0.5,
|
||||
}
|
||||
designs = {20.0: dict(hand), 40.0: dict(hand)}
|
||||
keys = (*CARRY_INPUT_KEYS, "shoulder_extend_left_m")
|
||||
inputs, _, dropped = carry_cross_inputs(old_keys, new_keys, designs, keys, (), SEAT_KEYS, seats)
|
||||
assert inputs[20.0] == hand
|
||||
assert inputs[40.5] == {"ground_type": "soil", "shoulder_extend_left_m": 0.5}
|
||||
assert dropped == 0
|
||||
# seats 없음 = 옛 동작(손 상단측은 안 넘김 · 자리 칸 셋은 넘김)
|
||||
old, _, _ = carry_cross_inputs(old_keys, new_keys, designs, keys, ())
|
||||
assert old[40.5] == {k: v for k, v in hand.items() if k not in ("section_mode", "ditch_side")}
|
||||
assert station_seat({"chainage_m": 1}) is None
|
||||
|
||||
|
||||
def test_기본_설계가_넘긴_손_단면_유형으로_셈(tmp_path, monkeypatch):
|
||||
"""엔진 입력 = 넘긴 단면 유형 · 측구 방향(결과 단면 유형은 엔진이 지형으로 다시 판정)."""
|
||||
import B06_Section.B06_Section_Router_Design as design_module
|
||||
|
||||
seen: list[tuple[str, object]] = []
|
||||
real = design_module.compute_cross_design
|
||||
|
||||
def spy(*args, **kwargs):
|
||||
seen.append((kwargs["section_mode"], kwargs.get("ditch_side")))
|
||||
return real(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(design_module, "compute_cross_design", spy)
|
||||
route = tmp_path / "routes" / "r1"
|
||||
(route / "longitudinal").mkdir(parents=True)
|
||||
(route / "cross_sections").mkdir()
|
||||
profile = [
|
||||
{"chainage_m": 0.0, "elevation_m": 100.0},
|
||||
{"chainage_m": 40.0, "elevation_m": 100.0},
|
||||
]
|
||||
relative = "routes/r1/longitudinal/longitudinal.json"
|
||||
stations = [{"chainage_m": 20.0, "uphill_side": "left"}]
|
||||
(tmp_path / relative).write_text(
|
||||
json.dumps({"stations": stations, "design_profiles": [{"samples": profile}]}), "utf-8"
|
||||
)
|
||||
samples = [
|
||||
{"offset_m": float(o), "elevation_m": 100.0 + 0.3 * o, "valid": True}
|
||||
for o in range(-15, 16)
|
||||
]
|
||||
cross = {"chainage_m": 20.0, "samples": samples, "uphill_side": "left"}
|
||||
find_cross_file(route / "cross_sections", 20.0).write_text(json.dumps(cross), "utf-8")
|
||||
plain = compute_default_designs(tmp_path, relative, [20.0])
|
||||
hand = compute_default_designs(
|
||||
tmp_path,
|
||||
relative,
|
||||
[20.0],
|
||||
carried={20.0: {"section_mode": "both_cut", "ditch_side": "right"}},
|
||||
)
|
||||
assert plain and hand
|
||||
assert seen == [("left_cut", None), ("both_cut", "right")]
|
||||
Reference in New Issue
Block a user