feat(511-B B-3 판 종단 줄 엶): 계산 자리 profile 줄 = 고를 수 있음(기본값 both 그대로 · 판 파일 profile both) · 「서버」 = 묶음 planline 새 지반 첫 계획선 꼭짓점(profile.base) · profile 단계 계획선 짓기(profile.alignment)를 창구 → 서버 Node 같은 TS(design_calc_profile.ts) · both = 단계 동기 그대로(묶음 차례 바뀜 0) · 서버 실패 = 경고 · 브라우저 값 · 까닭 글 pair_B3 → 끄면 서버 Node — 요구 320-4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/* =============================================================================
|
||||
* design_calc_profile.ts
|
||||
* 계산 자리 「서버」 종단 길(PLAN 511-B B-3) — 판(`design_calc_sites.ts`)이 profile 묶음을 server 로 둔 때
|
||||
* 묶음 planline 의 새 지반 첫 계획선 꼭짓점(P1 · `profile.base`)과 profile 단계 계획선 짓기(P2 ~ P4 · P8 ·
|
||||
* `profile.alignment`)를 서버 Node 가 같은 TS 로 셈(창구 `POST /projects/{id}/calc`). both 면 브라우저 그대로.
|
||||
* 서버 실패(끊김 밖) = 경고 한 줄 · 브라우저 값.
|
||||
* ========================================================================== */
|
||||
|
||||
import { calcOnServerNow } from "./design_calc_sites";
|
||||
import { serverCalc } from "./design_calc_server";
|
||||
import type { StageFn } from "./logic/design_pipeline_types";
|
||||
import {
|
||||
groundFollowingBasePvi,
|
||||
groundProfile,
|
||||
type GroundFollowingInput,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Grade_Ground";
|
||||
import { profileStage } from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
|
||||
import {
|
||||
emptyEdits,
|
||||
type AlignmentNode,
|
||||
type ProfileAlignment,
|
||||
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
|
||||
const KEY = "profile";
|
||||
const r6 = (value: number): number => Number(value.toFixed(6));
|
||||
|
||||
/** 서버 값 — 끊김은 그대로 던짐(묶음이 버림) · 그 밖 실패는 브라우저 값. */
|
||||
async function orLocal<T>(signal: AbortSignal, remote: () => Promise<T>, local: () => T) {
|
||||
try {
|
||||
return await remote();
|
||||
} catch (error) {
|
||||
if (signal.aborted) throw error;
|
||||
console.warn("[calc-server] profile 실패 — 브라우저 값", error);
|
||||
return local();
|
||||
}
|
||||
}
|
||||
|
||||
/** planline `groundPvi` — 서버면 첫 계획선 꼭짓점 약속(0.000001 m · `groundFollowingBasePvi` 꼴) · 아니면 null
|
||||
* (브라우저 셈 · 단계 동기). 판은 앞 ground 단계가 이미 받음(`calcOnServerNow`). */
|
||||
export function groundPviAt(
|
||||
input: GroundFollowingInput,
|
||||
signal: AbortSignal,
|
||||
): Promise<AlignmentNode[] | null> | null {
|
||||
if (!calcOnServerNow(KEY)) return null;
|
||||
const local = () => groundFollowingBasePvi(input);
|
||||
const profile = groundProfile(input.samples);
|
||||
if (!profile) return null;
|
||||
return orLocal(
|
||||
signal,
|
||||
async () => {
|
||||
const follow = {
|
||||
stations: input.stations,
|
||||
start_offset_m: input.startOffsetM ?? 0,
|
||||
end_offset_m: input.endOffsetM ?? 0,
|
||||
};
|
||||
const inputs = { profile: { ...profile, policy: input.policy, ground_following: follow } };
|
||||
const results = await serverCalc(KEY, ["profile.base"], inputs, signal);
|
||||
const nodes = results["profile.base"] as AlignmentNode[] | null;
|
||||
return (
|
||||
nodes?.map((node) => ({
|
||||
chainage_m: r6(node.chainage_m),
|
||||
elevation_m: r6(node.elevation_m),
|
||||
})) ?? null
|
||||
);
|
||||
},
|
||||
local,
|
||||
);
|
||||
}
|
||||
|
||||
/** profile 단계 — 첫 계획선 + 계획고 편집 → 종단(서버면 `profile.alignment` · 아니면 동기 그대로). */
|
||||
export const profileAt: StageFn<"profile"> = (ctx) => {
|
||||
const base = ctx.out.planline;
|
||||
if (!base || !calcOnServerNow(KEY)) return profileStage(ctx);
|
||||
return orLocal(
|
||||
ctx.signal,
|
||||
async () => {
|
||||
const profile = {
|
||||
chainage: base.chainage,
|
||||
ground: base.ground,
|
||||
policy: base.policy,
|
||||
stations: base.stations,
|
||||
base_pvi: base.basePvi,
|
||||
edits: ctx.inputs.alignment ?? emptyEdits(),
|
||||
only_explicit_curves: base.onlyExplicitCurves,
|
||||
};
|
||||
const results = await serverCalc(KEY, ["profile.alignment"], { profile }, ctx.signal);
|
||||
return results["profile.alignment"] as ProfileAlignment;
|
||||
},
|
||||
() => profileStage(ctx) as ProfileAlignment,
|
||||
);
|
||||
};
|
||||
@@ -87,9 +87,9 @@ import {
|
||||
createFollowStage,
|
||||
createPlanlineStage,
|
||||
gradeRange,
|
||||
profileStage,
|
||||
routeRange,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
|
||||
import { groundPviAt, profileAt } from "./design_calc_profile";
|
||||
import {
|
||||
draftSectionDetail,
|
||||
shapeStatus,
|
||||
@@ -299,8 +299,9 @@ function createPagePipeline(
|
||||
savedBase: (at) => options.plantedBase?.(savedSource(), at, types) ?? savedSource().base,
|
||||
savedLine,
|
||||
storedPvi,
|
||||
groundPvi: groundPviAt, // 계산 자리 「서버」 = 서버 Node(511-B B-3)
|
||||
}),
|
||||
profile: profileStage,
|
||||
profile: profileAt,
|
||||
// 단면 유형 = 새 지형 상단측 · 소단 = 지금 구조물(365-5) · 저장 노선 = 다시 셈 창구 한 벌(365-4) · 엔진은
|
||||
// 창구 출력을 앞 결과로 안 이어 씀(초안 = 새로고침)
|
||||
cross: (ctx) => {
|
||||
|
||||
@@ -52,7 +52,11 @@ import {
|
||||
} from "./B05_Profile_Grade_Carry";
|
||||
import { seatBornNodes } from "./B05_Profile_Grade_Seat";
|
||||
import { SECTION_STATION_INTERVAL_M } from "./B05_Profile_Sections_Stations";
|
||||
import { groundFollowingBasePvi, groundProfile } from "./B05_Profile_Grade_Ground";
|
||||
import {
|
||||
groundFollowingBasePvi,
|
||||
groundProfile,
|
||||
type GroundFollowingInput,
|
||||
} from "./B05_Profile_Grade_Ground";
|
||||
import { DEFAULT_CURVE_LENGTH_M } from "./B05_Profile_Profile_CurveLaw";
|
||||
|
||||
const xy = (line: ReadonlyArray<Vertex>): Array<{ x: number; y: number }> =>
|
||||
@@ -295,6 +299,12 @@ export interface PlanlineDeps {
|
||||
savedLine(): Vertex[] | null;
|
||||
/** 노선 초안 첫 계획선 꼭짓점 = 서버 저장 종단 — 보기가 심은 측점(초안 자리 관 등)에 안 흔들림(365-5) · 없으면 `savedBase`. */
|
||||
storedPvi?(): AlignmentBase["basePvi"] | null;
|
||||
/** 바뀐 자리 새 지반 첫 계획선 꼭짓점 — 계산 자리 「서버」 면 서버 Node 약속(511-B B-3) · null = 브라우저
|
||||
* `groundFollowingBasePvi`(단계 동기 그대로). */
|
||||
groundPvi?(
|
||||
input: GroundFollowingInput,
|
||||
signal: AbortSignal,
|
||||
): Promise<AlignmentBase["basePvi"] | null> | null;
|
||||
}
|
||||
|
||||
export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
||||
@@ -311,13 +321,6 @@ export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
||||
}));
|
||||
const profile = groundProfile(samples);
|
||||
if (!profile) return saved; // 못 서는 땅 — 저장 기준 그대로(서버가 [저장] 때 다시 셈)
|
||||
// 바뀐 자리 새 꼭짓점 = 서버 새 지반 첫 계획선(기본 L = config 짝 · 초기값 직선화 · 438)
|
||||
const pviAt = (stations: number[]) =>
|
||||
groundFollowingBasePvi({
|
||||
samples,
|
||||
stations,
|
||||
policy: { ...saved.policy, default_curve_length_m: DEFAULT_CURVE_LENGTH_M },
|
||||
}) ?? [];
|
||||
const savedLine = deps.savedLine();
|
||||
const span = savedLine ? changedSpan(xy(savedLine), xy(route.line)) : null;
|
||||
const savedPvi = deps.storedPvi?.() ?? saved.basePvi;
|
||||
@@ -344,33 +347,40 @@ export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
||||
...ground.stations.filter((station) => !born.includes(station.chainageM)),
|
||||
...gone.map((chainageM) => ({ chainageM, kind: "structure" as const, id: null })),
|
||||
];
|
||||
const carried = span
|
||||
? carryBase(
|
||||
xy(savedLine!),
|
||||
xy(route.line),
|
||||
savedPvi,
|
||||
pviAt(homeStations.map((station) => station.chainageM)),
|
||||
)
|
||||
: savedPvi;
|
||||
const home = span
|
||||
? seatRouteMoved(
|
||||
baseOf(carried),
|
||||
routeMovedStations(homeStations, xy(savedLine!), xy(route.line)),
|
||||
edited.map(Number),
|
||||
)
|
||||
: carried;
|
||||
const step = gone.length || born.length ? followMovedNodes(home, gone, born, edited) : null;
|
||||
return {
|
||||
policy: saved.policy, // 옛 기본 L 이어받음(서버 `carry_policy` 짝)
|
||||
onlyExplicitCurves: saved.onlyExplicitCurves,
|
||||
basePvi: step ? seatBornNodes(baseOf(home), step.nodes, step.seat, step.held) : home,
|
||||
chainage: profile.chainage,
|
||||
ground: profile.ground,
|
||||
stations: ground.stations.map((station) => ({
|
||||
station_id: stationIdOf(station.chainageM),
|
||||
chainage_m: station.chainageM,
|
||||
})),
|
||||
const finish = (groundPvi: AlignmentBase["basePvi"]): AlignmentBase => {
|
||||
const carried = span
|
||||
? carryBase(xy(savedLine!), xy(route.line), savedPvi, groundPvi)
|
||||
: savedPvi;
|
||||
const home = span
|
||||
? seatRouteMoved(
|
||||
baseOf(carried),
|
||||
routeMovedStations(homeStations, xy(savedLine!), xy(route.line)),
|
||||
edited.map(Number),
|
||||
)
|
||||
: carried;
|
||||
const step = gone.length || born.length ? followMovedNodes(home, gone, born, edited) : null;
|
||||
return {
|
||||
policy: saved.policy, // 옛 기본 L 이어받음(서버 `carry_policy` 짝)
|
||||
onlyExplicitCurves: saved.onlyExplicitCurves,
|
||||
basePvi: step ? seatBornNodes(baseOf(home), step.nodes, step.seat, step.held) : home,
|
||||
chainage: profile.chainage,
|
||||
ground: profile.ground,
|
||||
stations: ground.stations.map((station) => ({
|
||||
station_id: stationIdOf(station.chainageM),
|
||||
chainage_m: station.chainageM,
|
||||
})),
|
||||
};
|
||||
};
|
||||
// 바뀐 자리 새 꼭짓점 = 서버 새 지반 첫 계획선(기본 L = config 짝 · 초기값 직선화 · 438) · 계산 자리
|
||||
// 「서버」 면 서버 Node(`groundPvi` · 511-B B-3)
|
||||
const input = {
|
||||
samples,
|
||||
stations: homeStations.map((station) => station.chainageM),
|
||||
policy: { ...saved.policy, default_curve_length_m: DEFAULT_CURVE_LENGTH_M },
|
||||
};
|
||||
const remote = span ? deps.groundPvi?.(input, ctx.signal) : null;
|
||||
if (remote) return remote.then((nodes) => finish(nodes ?? []));
|
||||
return finish(span ? (groundFollowingBasePvi(input) ?? []) : []);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -46,12 +46,13 @@ GROUPS: tuple[dict[str, Any], ...] = (
|
||||
"fixed": "both",
|
||||
"why": "pair_B2",
|
||||
},
|
||||
# 종단 = 짝 모음 끝(B-3 · 서버 Node `profile.base` · `profile.alignment`) — 엶
|
||||
{
|
||||
"key": "profile",
|
||||
"nos": ["P1", "P2", "P3", "P4", "P8"],
|
||||
"switchable": False,
|
||||
"fixed": "both",
|
||||
"why": "pair_B3",
|
||||
"switchable": True,
|
||||
"fixed": None,
|
||||
"why": None,
|
||||
},
|
||||
{
|
||||
"key": "cross",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"corridor_mesh": "both",
|
||||
"quantity": "both",
|
||||
"carry": "both",
|
||||
"profile": "both",
|
||||
"fixed_browser": "both"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ def test_묶음_여덟_줄_등록부_전부():
|
||||
"corridor_mesh",
|
||||
"quantity",
|
||||
"carry",
|
||||
"profile", # 511-B B-3 엶
|
||||
"fixed_browser",
|
||||
} # 511-A 셋 더
|
||||
for group in calc_sites.GROUPS:
|
||||
|
||||
@@ -79,7 +79,6 @@ def test_판_다섯_줄_잠금_기본값_그대로():
|
||||
"route": (["R1", "R3"], "pair_B5"),
|
||||
"follow": (["P5", "P6", "F1", "F2"], "pair_B5"),
|
||||
"stations": (["R5", "R6", "R7", "F3"], "pair_B2"),
|
||||
"profile": (["P1", "P2", "P3", "P4", "P8"], "pair_B3"),
|
||||
"cross": (["G2", "G4", "C1", "C2", "C3", "C5", "C6", "F4", "F5", "F6"], "pair_B4"),
|
||||
}
|
||||
for key, (nos, why) in expected.items():
|
||||
@@ -93,6 +92,7 @@ def test_판_다섯_줄_잠금_기본값_그대로():
|
||||
"corridor_mesh",
|
||||
"quantity",
|
||||
"carry",
|
||||
"profile", # B-3 엶(511-B · test_511_b3_profile_node)
|
||||
"fixed_browser",
|
||||
}
|
||||
committed = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
@@ -103,7 +103,7 @@ def test_판_다섯_줄_잠금_기본값_그대로():
|
||||
).stdout
|
||||
assert calc_sites.PATH.read_bytes().replace(b"\r\n", b"\n") == committed.replace(b"\r\n", b"\n")
|
||||
locale = (ROOT / "ui_template" / "ui_template_locale_m1.ts").read_text(encoding="utf-8")
|
||||
for n in (2, 3, 4, 5):
|
||||
for n in (2, 4, 5):
|
||||
assert f'B01_CalcSites_Why_pair_B{n}: ["짝 모으는 중 · B-{n}"' in locale
|
||||
|
||||
|
||||
|
||||
@@ -115,8 +115,11 @@ export const ui_locales_m1 = {
|
||||
"끄면 잠잠 뒤 서버 셈 · F10 = 브라우저 부름 없음(판만)",
|
||||
"Off = server calc after a pause · F10 = no browser call",
|
||||
],
|
||||
B01_CalcSites_Why_profile: [
|
||||
"끄면 첫 계획선 · 계획선 짓기를 서버 Node 가 셈(조작 중 왕복)",
|
||||
"Off = server Node builds the first grade line · grade line (round trip while editing)",
|
||||
],
|
||||
B01_CalcSites_Why_pair_B2: ["짝 모으는 중 · B-2", "Merging pairs · B-2"],
|
||||
B01_CalcSites_Why_pair_B3: ["짝 모으는 중 · B-3", "Merging pairs · B-3"],
|
||||
B01_CalcSites_Why_pair_B4: ["짝 모으는 중 · B-4", "Merging pairs · B-4"],
|
||||
B01_CalcSites_Why_pair_B5: ["짝 모으는 중 · B-5", "Merging pairs · B-5"],
|
||||
B01_CalcSites_Why_form_default: [
|
||||
|
||||
Reference in New Issue
Block a user