Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -27,7 +27,7 @@ import { createDesignPipeline, publishToStore, type DesignPipeline } from "./log
|
||||
import { SOURCE_INPUT } from "./logic/design_pipeline_types";
|
||||
export { pipelineWork } from "./logic/design_pipeline";
|
||||
import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline_cross";
|
||||
import { createStationsStage, stationDiff } from "./logic/design_pipeline_stations";
|
||||
import { createStationsStage, snapOptions, stationDiff } from "./logic/design_pipeline_stations";
|
||||
import type {
|
||||
ChangeSource,
|
||||
FollowPatch,
|
||||
@@ -298,12 +298,7 @@ function createPagePipeline(
|
||||
route.curves,
|
||||
(patch.pipes ?? inputs.pipes ?? []) as PlanPipe[],
|
||||
(patch.structures ?? inputs.structures ?? []) as PlanStructure[],
|
||||
{
|
||||
intervalM: options.stationIntervalM,
|
||||
types: enabled,
|
||||
stationSnap: inputs.route?.stationSnap ?? false,
|
||||
stationSnapStep: inputs.route?.stationSnapStep,
|
||||
},
|
||||
{ intervalM: options.stationIntervalM, types: enabled, ...snapOptions(inputs.route, plan) },
|
||||
).map((station) => station.chainageM),
|
||||
});
|
||||
/** 다시 셈 창구가 낸 cross 출력(365-4) — 엔진 재사용에서 뺌. */
|
||||
@@ -342,7 +337,11 @@ function createPagePipeline(
|
||||
};
|
||||
return followStage({ ...ctx, inputs });
|
||||
},
|
||||
stations: createStationsStage({ intervalM: options.stationIntervalM, types: () => enabled }),
|
||||
stations: createStationsStage({
|
||||
intervalM: options.stationIntervalM,
|
||||
types: () => enabled,
|
||||
saved: () => plan,
|
||||
}),
|
||||
ground: createGroundStage(groundDeps),
|
||||
planline: createPlanlineStage({
|
||||
savedBase: (at) => options.plantedBase?.(savedSource(), at, types) ?? savedSource().base,
|
||||
|
||||
@@ -220,6 +220,8 @@ export async function saveDesign(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 상단측은 옛 누가거리 열쇠 — 노선이 바뀌면 횡단 차례(`carryAfterRouteSave`)가 새 측점으로 옮긴 뒤 보냄(443-2)
|
||||
if (step === "uphill" && routeSaved) continue;
|
||||
// 노선이 바뀌면 화면 몫 일(옛 노선 판 목록 · 계획고 판)은 건너뛰고 기본 창구만 새 노선으로.
|
||||
const own = routeSaved ? undefined : ctx.steps?.[step];
|
||||
const base = defaultStep(step, ctx.projectId, routeId, fromRouteId);
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
import type {
|
||||
PipelineRunRecord,
|
||||
PipelineStation,
|
||||
RouteDraft,
|
||||
StageContext,
|
||||
StageFn,
|
||||
} from "./design_pipeline_types";
|
||||
@@ -41,12 +42,30 @@ export function reuseOutside(
|
||||
});
|
||||
}
|
||||
|
||||
/** stations 단계 — 입력 관 · 구조물(follow 뒤 누가거리) · 그린 노선 · 곡선표 · 노선 초안 스냅. */
|
||||
/** 저장 노선의 측점 스냅 설정(`RoutePlanResponse` 칸). */
|
||||
export interface SavedSnap {
|
||||
station_snap?: boolean;
|
||||
station_snap_step?: number | null;
|
||||
}
|
||||
|
||||
/** 측점 스냅 설정 — 노선 초안이 있으면 그 값 · 없으면 저장 노선 값(441 · 저장 스냅 노선 측점 어긋남). */
|
||||
export function snapOptions(
|
||||
draft: RouteDraft | null | undefined,
|
||||
saved: SavedSnap | null | undefined,
|
||||
): Pick<StationsOptions, "stationSnap" | "stationSnapStep"> {
|
||||
return draft
|
||||
? { stationSnap: draft.stationSnap, stationSnapStep: draft.stationSnapStep }
|
||||
: { stationSnap: saved?.station_snap ?? false, stationSnapStep: saved?.station_snap_step };
|
||||
}
|
||||
|
||||
/** stations 단계 — 입력 관 · 구조물(follow 뒤 누가거리) · 그린 노선 · 곡선표 · 스냅(초안 · 없으면 저장 노선). */
|
||||
export function createStationsStage(
|
||||
options: Omit<StationsOptions, "stationSnap" | "stationSnapStep" | "types"> & {
|
||||
types: () => ReadonlyArray<PlanType>;
|
||||
saved?: () => SavedSnap | null;
|
||||
},
|
||||
): StageFn<"stations"> {
|
||||
const { saved, ...rest } = options;
|
||||
return (ctx: StageContext<"stations">) => {
|
||||
const route = ctx.out.route;
|
||||
if (!route) return [];
|
||||
@@ -55,12 +74,7 @@ export function createStationsStage(
|
||||
route.curves,
|
||||
(ctx.inputs.pipes as PlanPipe[] | null) ?? [],
|
||||
(ctx.inputs.structures as PlanStructure[] | null) ?? [],
|
||||
{
|
||||
...options,
|
||||
types: options.types(),
|
||||
stationSnap: ctx.inputs.route?.stationSnap ?? false,
|
||||
stationSnapStep: ctx.inputs.route?.stationSnapStep,
|
||||
},
|
||||
{ ...rest, types: options.types(), ...snapOptions(ctx.inputs.route, saved?.()) },
|
||||
);
|
||||
return reuseOutside(next, ctx.prev, ctx.range);
|
||||
};
|
||||
|
||||
@@ -544,6 +544,7 @@ export async function mountRouteEditor(
|
||||
if (base) markEdited();
|
||||
return base !== undefined;
|
||||
},
|
||||
offStation: () => (stationSnap ? fit.offStationCount(plannedLine, curveInfo, snap()) : 0),
|
||||
});
|
||||
|
||||
bindNodeSplice({
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* 실패하면 서버가 옛 노선으로 되살리고 화면 초안은 그대로 남는다(오류 한 줄은 부르는 쪽).
|
||||
* ========================================================================== */
|
||||
|
||||
import { clearDrafts, clearResults } from "../A00_Common/b_page_state";
|
||||
import { clearDrafts, clearResults, readState } from "../A00_Common/b_page_state";
|
||||
import type { RouteDraft } from "../A00_Common/design/logic/design_pipeline_types";
|
||||
import { useDesignStore } from "../A00_Common/design/design_store";
|
||||
import { currentRoute, remountRoute } from "../A00_Common/router";
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { withCarriedEdits, withDraftAdjusts } from "../B06_Section/B06_Section_Section_Adjusts";
|
||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { pipeChainageOf } from "../A00_Common/design/design_pipe_ids";
|
||||
import { clearRouteLatestCache } from "./B05_Profile_Api_Fetch";
|
||||
import { clearRouteLatestCache, flushUphillOverrides } from "./B05_Profile_Api_Fetch";
|
||||
import {
|
||||
replanRoute,
|
||||
type RouteReplanResponse,
|
||||
@@ -180,9 +180,16 @@ export async function carryAfterRouteSave(
|
||||
// 옛 저장 관 자리에 남은 고침(초안으로 안 따라감) = 같은 관 id 의 새 자리
|
||||
const movedAt = (chainageM: number): number | null =>
|
||||
newAt(before.find((pipe) => Math.abs(pipe.chainage_m - chainageM) < 0.01)?.id);
|
||||
const uphillCount = (): number => Object.keys(readState("uphill", projectId) ?? {}).length;
|
||||
const uphillBefore = uphillCount();
|
||||
const outside =
|
||||
carryStationDrafts(projectId, fromRouteId, toRouteId, { quiet: true }) +
|
||||
carryChainageStates(projectId, fromRouteId, toRouteId, { keyAt: pipeAt, movedAt });
|
||||
// 상단측 = 새 측점으로 옮긴 것만 횡단 패치 앞에 보냄(443-2 · 옛 누가거리로 엉뚱한 측점에 안 얹힘)
|
||||
const uphillDropped = uphillBefore - uphillCount();
|
||||
if (uphillDropped > 0)
|
||||
showToast(`상단측 ${uphillDropped}곳 — 새 노선에 같은 측점이 없어 뺐습니다.`, "info");
|
||||
await flushUphillOverrides(projectId);
|
||||
// 옮긴 고침을 새 상세 설계 칸에 — 같은 [저장]의 횡단 패치가 실음(사면 쉬프트 · 조절 · 소단 깊이 · 417)
|
||||
withDraftAdjusts(detail, projectId, toRouteId);
|
||||
withCarriedEdits(detail, projectId, toRouteId);
|
||||
|
||||
@@ -31,6 +31,8 @@ interface Hold {
|
||||
start: Vertex;
|
||||
/** 스냅 미리보기 전 노선(맞추기 전) — 다음 걸음 · 하한 검사는 여기서(맞춤이 쌓이지 않음). */
|
||||
raw: Vertex[] | null;
|
||||
/** 누를 때 측점 밖 곡선 수(441) — 미리보기가 이보다 늘면 그 걸음 되돌림. */
|
||||
off: number;
|
||||
moved: boolean;
|
||||
}
|
||||
|
||||
@@ -74,8 +76,18 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () =
|
||||
params.draw();
|
||||
return;
|
||||
}
|
||||
now.moved = true;
|
||||
fitPreview(now, state.planned);
|
||||
if (now.raw && params.offStation() > now.off) {
|
||||
// 맞춰도 MC 가 측점 밖에 남는 자리 — 끌기와 같게 그 걸음만 되돌림(441)
|
||||
const planned = params.state().planned;
|
||||
now.raw.forEach((vertex, index) => (planned[index] = vertex));
|
||||
planned[now.index] = node;
|
||||
fitPreview(now, planned);
|
||||
status.textContent = `${params.routeHead()} — 곡선 가운데를 측점에 못 맞춰 더 못 옮깁니다.`;
|
||||
params.draw();
|
||||
return;
|
||||
}
|
||||
now.moved = true;
|
||||
status.textContent = `${params.routeHead()} — 방향키로 옮기는 중(뗄 때 한 걸음). ${params.curveHint()}`;
|
||||
params.draw();
|
||||
}
|
||||
@@ -123,6 +135,7 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () =
|
||||
before: state.planned.map(([x, y]): Vertex => [x, y]),
|
||||
start: [node[0], node[1]],
|
||||
raw: null,
|
||||
off: params.offStation(),
|
||||
moved: false,
|
||||
};
|
||||
hold = now;
|
||||
|
||||
@@ -106,6 +106,8 @@ export interface RouteEditPointerParams {
|
||||
/** 끄는 중 · 방향키 걸음 미리보기(390-5) — 스냅 켬이면 IP `index` 자리 그대로 MC 를 간격 배수에 ·
|
||||
* 맞췄으면 true(끔이면 false · 아무 일 없음). */
|
||||
fitDrag: (index: number) => boolean;
|
||||
/** 측점 밖 곡선 수(스냅 끔 = 0 · 441 끌기 · 방향키 막힘). */
|
||||
offStation: () => number;
|
||||
}
|
||||
|
||||
export interface RouteEditPointer {
|
||||
@@ -191,8 +193,10 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
/** 이번 끌기에서 **실제로 움직였나** — 그냥 눌러 고르기만 한 것은 되돌릴 걸음이 아니다
|
||||
* (2026-09-07 실화면: 노드를 클릭만 해도 [되돌리기]가 켜졌다). */
|
||||
let dragMoved = false;
|
||||
/** 스냅 미리보기 전 자리(390-5) — 하한 검사는 맞춘 선이 아닌 이것으로(맞춤이 IP 를 ±간격/2 밂). */
|
||||
/** 스냅 미리보기 전 자리(390-5) — 하한 검사는 맞춘 선이 아닌 이것으로(맞춤이 IP 를 ±간격 밂). */
|
||||
let dragRaw: Vertex[] | null = null;
|
||||
/** 누를 때 측점 밖 곡선 수(441) — 미리보기가 이보다 늘면 그 걸음 되돌림(못 맞추는 자리로 못 끎). */
|
||||
let dragOff = 0;
|
||||
/** 구조물 측점 마커(399) — 집기 차례 = 재기 다음 맨 앞(409) · 끌기 · 방향키 1 m. */
|
||||
const marks = bindStructureMarks(params, () => dragNode >= 0 || dragHandle !== null);
|
||||
|
||||
@@ -227,6 +231,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
// 교각점을 고정한 자리는 **고르기만** 되고 안 끌린다(2026-09-12 사용자 지시 ①).
|
||||
dragNode = hitNode >= 0 && state.apexLock[hitNode] !== true ? hitNode : -1;
|
||||
dragRaw = null;
|
||||
dragOff = params.offStation();
|
||||
dragHandle = hitNode >= 0 ? null : handleAt(px, py);
|
||||
dragMoved = false;
|
||||
if (hitNode >= 0) {
|
||||
@@ -353,7 +358,6 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
// **지키던 자리가 넘어가는 것만** 막는다(계획서 0-9 ④).
|
||||
const before = curveShortfalls(state.nodeInfo, state.limitRadiusM, state.limitArcM);
|
||||
const previous = state.planned[dragNode];
|
||||
dragMoved = true;
|
||||
state.planned[dragNode] = toMetric(px, py);
|
||||
params.markEdited(); // 곡선을 그 자리에서 다시 그린다 — 나머지 곡선은 그대로 남는다.
|
||||
const after = params.state();
|
||||
@@ -372,6 +376,17 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
return;
|
||||
}
|
||||
fitPreview(state.planned); // 스냅 켬 = 끄는 동안 MC 를 간격 배수에(390-5 · 놓을 때 정본)
|
||||
if (dragRaw && params.offStation() > dragOff) {
|
||||
// 맞춰도 MC 가 측점 밖에 남는 자리 — 하한 막힘과 같게 그 걸음만 되돌림(441)
|
||||
const now = params.state().planned;
|
||||
dragRaw.forEach((vertex, index) => (now[index] = vertex));
|
||||
now[dragNode] = previous;
|
||||
fitPreview(now);
|
||||
status.textContent = `${params.routeHead()} — 곡선 가운데를 측점에 못 맞춰 더 못 옮깁니다.`;
|
||||
params.draw();
|
||||
return;
|
||||
}
|
||||
dragMoved = true; // 막힘 검사 뒤 — 막힌 채 놓으면 되돌릴 걸음 없음(433-14)
|
||||
// 끄는 동안에도 상태줄이 살아 있어야 한다 — 예전에는 여기서 아무 말이 없어
|
||||
// 「곡선이 사라졌다」는 인상만 남았다(2026-09-07 사용자 지적 ②).
|
||||
status.textContent = `${params.routeHead()} — 옮기는 중. ${params.curveHint()}`;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 측점 스냅 켬 = 곡선 가운데(MC)를 일반측점에(366-3 · 요구 240-11) — 손 편집 평면.
|
||||
*
|
||||
* 추천 노선 맞춤(`B05_Profile_Engine_RouteInitial_Station.py` `_try` · `_solve`)을 줄인 꼴:
|
||||
* · `from`(`fitStart` = 고친 자리 앞)부터 끝까지 차례로 — 고정 아닌 교각점(IP)을 들어오는 접선 따라 ±간격/2 안에서
|
||||
* · `from`(`fitStart` = 고친 자리 앞)부터 끝까지 차례로 — 고정 아닌 교각점(IP)을 들어오는 접선 따라 ±간격 안에서
|
||||
* 밀어 MC(BC · EC 누가거리 가운데)를 가까운 일반측점(앞 MC 측점 + 간격 이상 · No.1 이상)에 둠.
|
||||
* · 곡선 하한을 새로 깨거나(`shortfallCrossed`) 앞 곡선 MC 를 흔들면 버림 → 그 곡선은 그대로.
|
||||
* · 반지름 · 켬끔은 안 바꿈 — IP 자리만. 합치기 · 곡선 끄기 · 되돌아가기 없음.
|
||||
@@ -113,7 +113,7 @@ function solve(
|
||||
if (!(length > 1e-9)) return null;
|
||||
const ux = (base[0] - before[0]) / length;
|
||||
const uy = (base[1] - before[1]) / length;
|
||||
const reach = limits.intervalM / 2;
|
||||
const reach = limits.intervalM; // 한가운데(18+9 ~ 19+1)도 가까운 측점까지(441 · 반 간격이면 못 닿음)
|
||||
const clamp = (d: number): number => Math.max(-reach, Math.min(reach, d));
|
||||
const at = (d: number) => {
|
||||
const next = planned.slice();
|
||||
|
||||
@@ -68,6 +68,7 @@ export const ROUTE_CARRY_STATES = {
|
||||
"detail-table": ["detail_table"],
|
||||
shoulderext: ["shoulder_extend_left_m", "shoulder_extend_right_m"],
|
||||
bermdepth: ["berm_depth_left_m", "berm_depth_right_m"],
|
||||
uphill: [], // 상단측(측구 방향) — 프로젝트 칸 · 설계 칸 없음 · 노선 바뀐 저장 때 새 측점으로(443-2)
|
||||
} as const;
|
||||
/** 구조물 조절 칸 — 관 · 구조물 열쇠 측점만 따라감(서버 `carry_cross_inputs` 와 같음). */
|
||||
const STRUCTURE_STATES = new Set<string>([
|
||||
|
||||
@@ -95,15 +95,17 @@ async function toggles(projectId, shift) {
|
||||
const steps = [];
|
||||
const run = async (tag) => {
|
||||
const updated = [];
|
||||
let calls = 0; // 받는 쪽 = 카드 다시 그림 + 종단 · 유토 다시 그림(design_shared) — 한 걸음에 한 번
|
||||
const out = await m.refreshSavedCross(
|
||||
{ projectId, routeId: R, detail, edits: EDITS, alignment },
|
||||
stations,
|
||||
(rows) => updated.push(...rows),
|
||||
(rows) => (calls++, updated.push(...rows)),
|
||||
);
|
||||
steps.push({
|
||||
tag,
|
||||
out: out ? Object.keys(out.byKey).length : null,
|
||||
updated,
|
||||
calls,
|
||||
...pick(target.design),
|
||||
});
|
||||
};
|
||||
@@ -126,6 +128,9 @@ async function toggles(projectId, shift) {
|
||||
m.writeCrossDesignChoice(projectId, R, target.chainage_m, { ...choice });
|
||||
await run(tag);
|
||||
}
|
||||
// 사면 이동 한 걸음(B06 ◀▶ 0.1 m · 깎기 쪽 · 441 · 요구 268-4)
|
||||
m.writeShoulderExtend(projectId, R, target.chainage_m, job.mode.split("_")[0], 0.1);
|
||||
await run("slope");
|
||||
return { target: target.chainage_m, steps };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/* PLAN 439 시험 묶음 진입점 — 저장 노선 cross 차례(카드 단추 → 다시 셈 · 측점 어긋남) · 진짜 모듈 · vite ssr. */
|
||||
export { refreshSavedCross } from "../../A00_Common/design/design_shared";
|
||||
export { writeCrossDesignChoice } from "../../B06_Section/B06_Section_Cross_Design_Session";
|
||||
export {
|
||||
writeCrossDesignChoice,
|
||||
writeShoulderExtend,
|
||||
} from "../../B06_Section/B06_Section_Cross_Design_Session";
|
||||
export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel";
|
||||
export { writeState } from "../../A00_Common/b_page_state";
|
||||
|
||||
@@ -78,4 +78,24 @@ out.move = {
|
||||
same: after.map((s) => byKey.get(s.key) === s),
|
||||
empty: run(null, null, null, null),
|
||||
};
|
||||
// 441 — 초안 없는 저장 노선 = 저장 스냅 설정 따름 · 초안이 있으면 초안 값(스냅 case = cases[1])
|
||||
const snapCase = job.cases[1];
|
||||
const savedRun = (route, saved) =>
|
||||
stage
|
||||
.createStationsStage({ types: () => job.types, saved: () => saved })({
|
||||
generation: 1,
|
||||
range: null,
|
||||
signal: new AbortController().signal,
|
||||
inputs: { route, pipes: job.pipes, structures: job.structures },
|
||||
out: { route: { line: snapCase.line, curves: snapCase.curves, chainage: [], lengthM: 0 } },
|
||||
before: {},
|
||||
prev: null,
|
||||
})
|
||||
.map((s) => s.chainageM);
|
||||
out.savedSnap = {
|
||||
saved: savedRun(null, { station_snap: true, station_snap_step: null }),
|
||||
savedOff: savedRun(null, { station_snap: false }),
|
||||
noPlan: savedRun(null, null),
|
||||
draftWins: savedRun({ stationSnap: false }, { station_snap: true }),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -61,7 +61,10 @@ const STUBS = {
|
||||
fetchDetailPipePoints: async () => (calls.push("pipes"), { pipe_points: [] }),
|
||||
},
|
||||
"../A00_Common/design/design_pipe_ids": { pipeChainageOf: () => null },
|
||||
"./B05_Profile_Api_Fetch": { clearRouteLatestCache: () => calls.push("latest:clear") },
|
||||
"./B05_Profile_Api_Fetch": {
|
||||
clearRouteLatestCache: () => calls.push("latest:clear"),
|
||||
flushUphillOverrides: async () => calls.push("uphill"), // 옮긴 뒤 · 횡단 패치 앞(443-2)
|
||||
},
|
||||
"./B05_Profile_Api_Replan": {
|
||||
replanRoute: async (_p, body) => {
|
||||
bodies.push(JSON.parse(JSON.stringify(body)));
|
||||
|
||||
@@ -128,6 +128,82 @@ const drag = Object.fromEntries(
|
||||
}),
|
||||
);
|
||||
|
||||
// ④ 끌기 훑기(441) — 편집 창 끌기 차례(`_Pointer`) 그대로: 하한 막힘 · 미리보기 측점 밖이 누를 때보다 늘면
|
||||
// 그 걸음 되돌림 · 놓을 때 정본 맞춤(`applyEdit`). 측점 No. · IP 셋 × 12 방향 × 3 거리 · 고정 R(곡선표 반경 못박음).
|
||||
const edits = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Edits.ts"));
|
||||
const copy = (c) => ({
|
||||
...c,
|
||||
planned: c.planned.map(([x, y]) => [x, y]),
|
||||
curveRadius: [...c.curveRadius],
|
||||
});
|
||||
function sweep(fixedR) {
|
||||
const start = columns(fit.fitCurveStations(columns(ROUTE), LIMITS, 1));
|
||||
if (fixedR)
|
||||
start.curveRadius = edits
|
||||
.rebuildLine(copy(start), LIMITS)
|
||||
.nodes.map((n) => (n.radius_m === null ? null : Math.round(n.radius_m * 100) / 100));
|
||||
const floor = (built) =>
|
||||
edits.curveShortfalls(built.nodes, LIMITS.limitRadiusM, LIMITS.limitArcM);
|
||||
const off = (built) => fit.offStationCount(built.vertices, built.curves, INTERVAL);
|
||||
const tally = { drags: 0, released: 0, offAfter: 0, strayed: 0, worst: [] };
|
||||
for (const K of [2, 3, 4])
|
||||
for (let deg = 0; deg < 360; deg += 30)
|
||||
for (const reach of [9, 17, 26]) {
|
||||
const hist = copy(start);
|
||||
let st = copy(hist);
|
||||
let built = edits.rebuildLine(st, LIMITS);
|
||||
const offBefore = off(built);
|
||||
let raw = null;
|
||||
let movedOnce = false;
|
||||
const preview = () => {
|
||||
raw = st.planned.map(([x, y]) => [x, y]);
|
||||
st = { ...st, planned: fit.fitDrag(st, hist.planned, K, LIMITS) };
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
};
|
||||
const p0 = hist.planned[K];
|
||||
for (let t = 0.5; t <= reach + 1e-9; t += 0.5) {
|
||||
if (raw) raw.forEach((v, i) => (st.planned[i] = v));
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
const before = floor(built);
|
||||
const previous = st.planned[K];
|
||||
st.planned[K] = [
|
||||
p0[0] + Math.cos((deg * Math.PI) / 180) * t,
|
||||
p0[1] + Math.sin((deg * Math.PI) / 180) * t,
|
||||
];
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
if (edits.shortfallCrossed(before, floor(built))) {
|
||||
st.planned[K] = previous;
|
||||
preview();
|
||||
continue;
|
||||
}
|
||||
preview();
|
||||
if (off(built) > offBefore) {
|
||||
raw.forEach((v, i) => (st.planned[i] = v));
|
||||
st.planned[K] = previous;
|
||||
preview();
|
||||
tally.strayed += 1;
|
||||
continue;
|
||||
}
|
||||
movedOnce = true;
|
||||
}
|
||||
tally.drags += 1;
|
||||
if (!movedOnce) continue;
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
if (edits.floorStop(hist, st.planned, built.nodes, LIMITS)) continue;
|
||||
const from = fit.fitStart(hist, st);
|
||||
st = { ...st, planned: fit.fitCurveStations(st, LIMITS, from) };
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
tally.released += 1;
|
||||
const left = fit.offStationMcs(built.vertices, built.curves, INTERVAL);
|
||||
if (left.length) {
|
||||
tally.offAfter += 1;
|
||||
if (tally.worst.length < 5) tally.worst.push({ K, deg, reach, mc: left });
|
||||
}
|
||||
}
|
||||
return tally;
|
||||
}
|
||||
const swept = { auto: sweep(false), fixed: sweep(true) };
|
||||
|
||||
// ③ MC 측점 값 · 눈금 글
|
||||
const input = JSON.parse(process.argv[2] || '{"grid":[],"spans":[]}');
|
||||
const snapped = Object.fromEntries(
|
||||
@@ -147,5 +223,12 @@ const labels = {
|
||||
};
|
||||
|
||||
console.log(
|
||||
JSON.stringify({ keys, drag, snapped, labels, scale: [0.1, 1, 10, null].map(frame.mcScale) }),
|
||||
JSON.stringify({
|
||||
keys,
|
||||
drag,
|
||||
swept,
|
||||
snapped,
|
||||
labels,
|
||||
scale: [0.1, 1, 10, null].map(frame.mcScale),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* 443-2 시험 묶음 진입점 — 노선 바뀐 [저장] 때 상단측(uphill)을 측점 열쇠로 새 측점에 옮김.
|
||||
* Node 헬퍼 = helper_443_2_uphill_carry.mjs · 짝 = test_443_2_uphill_carry.py.
|
||||
*/
|
||||
export { carryChainageStates, useStationKeyList } from "../../B06_Section/B06_Section_Station_Keys";
|
||||
export { readState, writeState } from "../../A00_Common/b_page_state";
|
||||
@@ -0,0 +1,61 @@
|
||||
/* 443-2 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_443_2_entry.ts)을 부르고 결과를 JSON 한 벌로.
|
||||
* node helper_443_2_uphill_carry.mjs <묶음.mjs>
|
||||
*/
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const store = () => {
|
||||
const m = new Map();
|
||||
return {
|
||||
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
||||
setItem: (k, v) => m.set(k, String(v)),
|
||||
removeItem: (k) => m.delete(k),
|
||||
key: (i) => [...m.keys()][i] ?? null,
|
||||
get length() {
|
||||
return m.size;
|
||||
},
|
||||
clear: () => m.clear(),
|
||||
};
|
||||
};
|
||||
globalThis.window = globalThis;
|
||||
globalThis.sessionStorage = store();
|
||||
globalThis.localStorage = store();
|
||||
const any = new Proxy(function () {}, {
|
||||
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
|
||||
apply: () => any,
|
||||
construct: () => any,
|
||||
});
|
||||
globalThis.document = any;
|
||||
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||
|
||||
const m = await import(pathToFileURL(process.argv[2]).href);
|
||||
const P = "p443";
|
||||
const grid = (pairs) => pairs.map(([key, chainageM]) => ({ key, chainageM }));
|
||||
// 옛 노선 = 20 m 일반측점 넷 · 새 노선 = 같은 번호가 밀림(21.5 · 43) · grid:3(60 m) 사라짐
|
||||
m.useStationKeyList(
|
||||
P,
|
||||
7,
|
||||
grid([
|
||||
["end:BP", 0],
|
||||
["grid:1", 20],
|
||||
["grid:2", 40],
|
||||
["grid:3", 60],
|
||||
]),
|
||||
);
|
||||
m.useStationKeyList(
|
||||
P,
|
||||
8,
|
||||
grid([
|
||||
["end:BP", 0],
|
||||
["grid:1", 21.5],
|
||||
["grid:2", 43],
|
||||
]),
|
||||
);
|
||||
m.writeState("uphill", { "20.000": "right", "40.000": "left", "60.000": "right" }, P);
|
||||
const outside = m.carryChainageStates(P, 7, 8);
|
||||
const moved = m.readState("uphill", P);
|
||||
// 세션이 없으면 그대로(빈 칸 안 만듦)
|
||||
m.writeState("uphill", null, P);
|
||||
const outsideEmpty = m.carryChainageStates(P, 7, 8);
|
||||
process.stdout.write(
|
||||
JSON.stringify({ outside, moved, outsideEmpty, empty: m.readState("uphill", P) }),
|
||||
);
|
||||
@@ -4,6 +4,7 @@
|
||||
- 저장 노선 cross 차례(`refreshSavedCross`) = 카드 자료(상세 설계)를 **늘** 다시 셈 —
|
||||
복합경사 · 측구 · 지반 단추마다 그 측점 설계선 · 면적이 바뀜.
|
||||
- 묶음 측점 = 상세 측점이면 돌려줌 · 1 mm 넘게 다르면 null(묶음 엔진) — 그래도 카드 자료는 고침.
|
||||
- 사면 이동 한 걸음도 같음 · 걸음마다 받는 쪽(카드 · 종단 · 유토 다시 그림) 한 번(441 · 요구 268-2 · 4).
|
||||
- 서버 폴백(선형 저장분 없음)에도 카드 선택이 실림(열쇠 = 측점 자리) · 서버는 저장분 위에 얹음.
|
||||
짝: `helper_266_card_toggle.mjs` · 진입점 `helper_266_card_toggle_entry.ts` ·
|
||||
지형 · 노선 틀 = `test_346_2_pipeline_cross_page.py`.
|
||||
@@ -99,7 +100,8 @@ def _check_toggles(case: dict, returned) -> None:
|
||||
steps = {step["tag"]: step for step in case["steps"]}
|
||||
assert steps["open"]["cut"] > 0.5 # 깎기 측점
|
||||
before = steps["open"]
|
||||
for tag in ("two_stage", "ditch_off", "soil"):
|
||||
assert all(step["calls"] == 1 for step in case["steps"]) # 걸음마다 다시 그림 한 번(441)
|
||||
for tag in ("two_stage", "ditch_off", "soil", "slope"):
|
||||
step = steps[tag]
|
||||
assert case["target"] in step["updated"], tag # 카드 다시 그림 대상
|
||||
assert step["line"] != before["line"], tag # 카드 설계선 바뀜
|
||||
|
||||
@@ -264,6 +264,17 @@ def test_스냅_측점은_원래_격자_번호(r):
|
||||
assert moved and all(abs(a - b) <= 10 for a, b in moved.values())
|
||||
|
||||
|
||||
def test_초안_없으면_저장_스냅_따름(r):
|
||||
"""441 — 초안 없는 저장 노선 측점 단계 = 저장 station_snap · 초안이 있으면 초안 값."""
|
||||
snapped = [s["chainageM"] for s in r["cases"][1]]
|
||||
plain = [s["chainageM"] for s in r["cases"][0]]
|
||||
assert snapped != plain
|
||||
got = r["savedSnap"]
|
||||
assert got["saved"] == snapped
|
||||
assert got["savedOff"] == plain and got["noPlan"] == plain
|
||||
assert got["draftWins"] == plain
|
||||
|
||||
|
||||
def test_노선_한_점_옮김_앞_구간_열쇠_그대로(r):
|
||||
m = r["move"]
|
||||
from_m = _first_changed_m()
|
||||
|
||||
@@ -82,6 +82,7 @@ def test_초안_넘김_차례(out):
|
||||
"pipes", # 관 열쇠 → 새 자리(서버가 옮긴 관)
|
||||
"carry:7->9:quiet",
|
||||
"carryAt:7->9", # 417 — 조절 · 측점 폭 · 상세 표도 새 노선 측점으로
|
||||
"uphill", # 443-2 — 상단측도 새 측점으로 옮긴 뒤 · 횡단 패치 앞
|
||||
"adjusts:9",
|
||||
"edits:9", # 옮긴 고침을 새 상세 설계 칸에 → 같은 [저장] 횡단 패치
|
||||
"crossSave:9",
|
||||
|
||||
@@ -80,7 +80,17 @@ def test_끄는_동안_MC_가_간격_배수(got, step) -> None:
|
||||
drag = got["drag"][step]
|
||||
assert drag["off"] == 0
|
||||
assert max(drag["residues"]) <= drag["tol"]
|
||||
assert drag["dragMoved"] <= float(step) / 2 + 1e-6 # 끈 IP 는 접선 따라 ±간격/2 안만
|
||||
assert drag["dragMoved"] <= float(step) + 1e-6 # 끈 IP 는 접선 따라 ±간격 안만(441)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["auto", "fixed"])
|
||||
def test_끌기_훑기_놓은_뒤_측점_밖_0(got, mode) -> None:
|
||||
"""441 — 측점 No. 끌기(IP 셋 × 12 방향 × 3 거리) · 놓은 뒤 MC 가 다 측점 위 · 맞춤이 늘 닿음
|
||||
(반 간격 한도면 18+9 ~ 19+1 에서 못 맞춰 막힘 걸음이 생김)."""
|
||||
swept = got["swept"][mode]
|
||||
assert swept["drags"] == 108 and swept["released"] > 0
|
||||
assert swept["offAfter"] == 0, swept["worst"]
|
||||
assert swept["strayed"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
|
||||
@@ -101,6 +111,12 @@ def test_끌기_방향키_편집기_이음() -> None:
|
||||
# 하한 검사는 맞추기 전 노선으로(맞춤이 IP 를 ±간격/2 밀어 10 m · 측점 No. 에서 헛걸림)
|
||||
assert "dragRaw.forEach((vertex, index) => (state.planned[index] = vertex))" in pointer
|
||||
assert "now.raw.forEach((vertex, index) => (state.planned[index] = vertex))" in keys
|
||||
# 미리보기 측점 밖이 누를 때보다 늘면 그 걸음 되돌림 · 막힘 검사 뒤에 옮김 표시(441 · 433-14)
|
||||
assert "params.offStation() > dragOff" in pointer and "params.offStation() > now.off" in keys
|
||||
move = pointer[pointer.index("const previous = state.planned[dragNode];") :]
|
||||
assert move.index("shortfallCrossed(") < move.index("params.offStation() > dragOff")
|
||||
assert move.index("params.offStation() > dragOff") < move.index("dragMoved = true;")
|
||||
assert "offStation: () => (stationSnap ? fit.offStationCount(" in edit
|
||||
assert edit.count("\n") <= 695
|
||||
|
||||
|
||||
@@ -193,6 +209,9 @@ def test_서버_측점_셈이_저장_간격을_씀() -> None:
|
||||
page = (design / "design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||
assert "stationSnapStep: routePlan.station_snap_step" in page
|
||||
stage = (design / "logic" / "design_pipeline_stations.ts").read_text(encoding="utf-8")
|
||||
assert "stationSnapStep: ctx.inputs.route?.stationSnapStep" in stage
|
||||
assert "...snapOptions(ctx.inputs.route, saved?.())" in stage # 초안 · 없으면 저장 노선(441)
|
||||
assert "stationSnapStep: draft.stationSnapStep" in stage
|
||||
assert "stationSnapStep: saved?.station_snap_step" in stage
|
||||
assert "saved: () => plan" in page and "...snapOptions(inputs.route, plan)" in page
|
||||
apply = (B05 / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
|
||||
assert "stationSnap: true, stationSnapStep: null" in apply # 추천 = 측점 No.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""PLAN 443-2 — 노선 바꾼 [저장]의 상단측(uphill).
|
||||
|
||||
- 옛 누가거리 열쇠 상단측 = 측점 열쇠(옮김 목록 `ROUTE_CARRY_STATES` · 415 · 417 규칙)로 새 노선 측점에.
|
||||
- 새 노선에 같은 측점이 없으면 뺌 · 수를 돌려줌(「노선 밖 고침」 · 상단측 알림).
|
||||
- 저장 한 줄기: 노선을 저장했으면 상단측 차례는 건너뛰고 `carryAfterRouteSave` 가 옮긴 뒤 · 횡단 패치 앞에 보냄.
|
||||
Node 헬퍼 `helper_443_2_uphill_carry.mjs` 가 vite ssr 로 묶은 진짜 모듈(`helper_443_2_entry.ts`)을 부름.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_443_2_uphill_carry.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory):
|
||||
work = tmp_path_factory.mktemp("b443_2")
|
||||
build = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_443_2_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"))
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_443_2_uphill_carry.mjs"), str(bundle)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_상단측이_새_노선_같은_측점으로(out):
|
||||
assert out["moved"] == {"21.50": "right", "43.00": "left"}
|
||||
|
||||
|
||||
def test_못_맞춘_상단측은_빼고_수를_돌려줌(out):
|
||||
assert out["outside"] == 1
|
||||
|
||||
|
||||
def test_세션_없으면_그대로(out):
|
||||
assert out["outsideEmpty"] == 0 and out["empty"] is None
|
||||
|
||||
|
||||
def test_노선_저장이면_상단측_차례는_옮긴_뒤():
|
||||
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
|
||||
assert 'if (step === "uphill" && routeSaved) continue;' in save
|
||||
apply = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Apply.ts").read_text("utf-8")
|
||||
body = apply.split("export async function carryAfterRouteSave(")[1].split("\n}\n")[0]
|
||||
carry = body.index("carryChainageStates(")
|
||||
flush = body.index("await flushUphillOverrides(projectId)")
|
||||
assert carry < flush < body.index("saveCachedCrossPatches(") # 옮긴 뒤 · 횡단 패치 앞
|
||||
assert "상단측 ${uphillDropped}곳" in body # 못 맞춤 알림
|
||||
@@ -102,8 +102,7 @@ def test_노선이_바뀌면_노선_먼저_뒤_차례는_새_노선(out):
|
||||
"plusStations",
|
||||
'alignment:9:{"30.000":0.2}',
|
||||
"culvert:aislo:draft:culvertopt:p1:7",
|
||||
"uphill",
|
||||
"carry:7->9",
|
||||
"carry:7->9", # 상단측은 넘김(carryAfterRouteSave)이 새 측점으로 옮긴 뒤 보냄(443-2)
|
||||
"summary:9:2",
|
||||
"toast:info",
|
||||
"done:false",
|
||||
|
||||
Reference in New Issue
Block a user