Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-10-18 21:50:26 +09:00
22 changed files with 262 additions and 206 deletions
+6 -18
View File
@@ -6,9 +6,8 @@
* route · ground `B05_Profile/logic/B05_Profile_Pipeline_RouteGround.ts`(sub8)
* follow · planline · profile `B05_Profile/logic/B05_Profile_Pipeline_Grade.ts`
* stations `design_pipeline_stations.ts` · footprint `B05_Profile_Footprint_Stage.ts`(sub6)
* cross · masshaul · haul `B06_Section_Cross_Stage` · `logic/B05_Profile_MassHaul_Stage` ·
* `B05_Profile_Api_HaulPlan`(sub2) · drainage 배수유역 판 `pipelineStage`(sub3) · corridor 3D 「낡음」.
* haul · corridor 는 노선 초안 동안만(저장 노선은 유토곡선 판 · 횡단 다시 셈이 맡음).
* cross `B06_Section_Cross_Stage` · drainage 배수유역 판 `pipelineStage`(sub3) · corridor 3D 「낡음」.
* corridor 는 노선 초안 동안만 · 유토곡선 · 흙 배분은 유토곡선 판이 셈(454).
*
* 입력 — 노선 = 편집기 다리(`RouteDraftBridge` · 캐시 칸 route-draft) · 계획고 = 종단도 편집
* (`pipelineGrade` · 노선 초안 동안은 alignment-draft 노선 `DRAFT_ROUTE_ID`) · 횡단 = 저장 상세의 측점
@@ -65,7 +64,6 @@ import {
fetchRoutePlan,
type RoutePlanResponse,
} from "../../B05_Profile/B05_Profile_Api_Replan";
import { createHaulStage } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
import { forgetPageData, pagePipes, pageStructures, trackPageBusy } from "./design_page_data";
import type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types";
@@ -98,10 +96,6 @@ import {
SECTION_STATION_INTERVAL_M,
} from "../../B05_Profile/logic/B05_Profile_Sections_Stations";
import { footprintStage } from "../../B05_Profile/logic/B05_Profile_Footprint_Stage";
import {
createMassHaulStage,
type MassHaulStageContext,
} from "../../B05_Profile/logic/B05_Profile_MassHaul_Stage";
import type {
PlanPipe,
PlanStructure,
@@ -122,7 +116,7 @@ interface PagePipeline {
bridge: RouteDraftBridge;
grade(edits: AlignmentEdits): void;
restart(): void;
earthwork(context: MassHaulStageContext | null): void;
earthwork(): void;
cross(): void;
reconcile(): Promise<number>;
ownsCross: boolean;
@@ -154,9 +148,8 @@ export const pipelineGrade = (projectId: string, edits: AlignmentEdits): void =>
/** 저장 정본이 바뀜(새로 받은 상세 · 새 노선) — 처음부터 다시. 갈아끼운 상세 · 같은 상세 다시 그림은 안 셈. */
export const restartPagePipeline = (projectId: string): void => pages.get(projectId)?.restart();
/** 유토곡선 설정(토량환산계수 · 자연방토 경사) — masshaul 단계 재료 · 표준단면이 막 왔으면 횡단 입력도. */
export const pipelineEarthwork = (projectId: string, context: MassHaulStageContext | null): void =>
pages.get(projectId)?.earthwork(context);
/** 유토곡선 설정(표준단면 포함)이 막 옴 — 횡단 입력을 다시 먹임. */
export const pipelineEarthwork = (projectId: string): void => pages.get(projectId)?.earthwork();
/** 횡단 고침(지반 유형 · 측구 · 암 · 소단 · 표준 — B06 · 365-2) — 세션 횡단 입력을 다시 모아 cross 부터. */
export const pipelineCross = (projectId: string): void => pages.get(projectId)?.cross();
@@ -195,7 +188,6 @@ function createPagePipeline(
let enabled: PlanType[] = [];
let savedStructures: PlanStructure[] = [];
let savedPipes: PlanPipe[] = [];
let earthwork: MassHaulStageContext | null = null;
/** 저장 노선 측점(열쇠) · 마지막으로 모은 횡단 입력(시설 제원 — 새 노선 상세가 씀). */
let savedStations: PipelineStation[] = [];
let crossSource: PipelineCrossSource | null = null;
@@ -280,7 +272,6 @@ function createPagePipeline(
};
const groundDeps = groundStageDeps(projectId);
const crossStage = createCrossStage({ grid: groundDeps.grid }); // 미교차 넓힘 = 같은 지형 격자(349-0j)
const haulStage = createHaulStage(projectId, () => savedSource().routeId ?? undefined);
const followStage = createFollowStage({
savedLine,
saved: () => {
@@ -379,8 +370,6 @@ function createPagePipeline(
});
},
footprint: footprintStage,
masshaul: createMassHaulStage(() => earthwork),
haul: (ctx) => (ctx.inputs.route ? haulStage(ctx) : (ctx.prev ?? null)),
drainage: link.side?.pipelineStage,
corridor: (ctx) => {
if (ctx.inputs.route) options.onCorridorStale?.();
@@ -678,8 +667,7 @@ function createPagePipeline(
// 갈아끼움 · 같은 상세 다시 그림은 안 셈 · 보기가 심은 측점은 묶음이 입력으로 셈(386-3)
if (canonChanged()) void loadPlan().then(start);
},
earthwork(context) {
earthwork = context;
earthwork() {
const cross = crossNow()?.input ?? null;
if (started && cross) void feed("cross", cross);
},
+1 -1
View File
@@ -522,7 +522,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
*/
setEarthworkContext(next: RouteMassHaulContext | null) {
massHaul.setContext(next);
pagePipeline.pipelineEarthwork(projectId, next);
pagePipeline.pipelineEarthwork(projectId);
drawSoon();
},
setSelectedStation(stationId: string | null) {
+2 -5
View File
@@ -39,10 +39,9 @@ export const BROWSER_STAGES: StageName[] = [
"profile",
"cross",
"footprint",
"masshaul",
];
/** 뒤 — 브라우저 단계가 끝나면 함께 띄우고 기다리지 않음. */
export const AFTER_STAGES: StageName[] = ["haul", "drainage", "corridor"];
export const AFTER_STAGES: StageName[] = ["drainage", "corridor"];
type Dep = StageName | `input.${InputKey}`;
@@ -56,15 +55,13 @@ export const STAGE_DEPS: Record<StageName, Dep[]> = {
profile: ["planline", "input.alignment"],
cross: ["profile", "ground", "input.cross", "input.pipes", "input.structures"],
footprint: ["cross", "ground"],
masshaul: ["cross"],
haul: ["masshaul"],
drainage: ["route", "input.pipes"],
corridor: ["profile", "cross"],
};
/** follow 가 고칠 수 있는 입력 — 노선 · 횡단은 안 됨(고리). */
const FOLLOW_KEYS: ReadonlyArray<keyof FollowPatch> = ["pipes", "structures", "alignment"];
/** 이 단계 뒤 한 번 쉼 — 종단이 먼저 그려지고 횡단 · 유토곡선이 그다음. */
/** 이 단계 뒤 한 번 쉼 — 종단이 먼저 그려지고 횡단이 그다음. */
const YIELD_AFTER = new Set<StageName>(["profile"]);
export const RUN_LOG_SIZE = 20;
@@ -9,9 +9,9 @@
* plusStations(추가 측점 · 429 ⑨)
* ↓ update(출처, 값, 범위) — 조작마다 차례 번호(generation) +1 · 옛 계산 · 옛 요청 버림
* 브라우저 즉시(차례대로 · 단계 사이마다 번호 확인 · 단계마다 결과 바로 내보냄)
* route → follow → stations → ground → planline → profile → cross → footprint → masshaul
* route → follow → stations → ground → planline → profile → cross → footprint
* 뒤(기다리지 않음 · 새 조작이 오면 AbortSignal 로 끊김 · 옛 번호 결과 버림)
* haul(흙 배분 · 서버 Node) · drainage(배수유역 「낡음」 표시) · corridor(3D 「낡음」 표시)
* drainage(배수유역 「낡음」 표시) · corridor(3D 「낡음」 표시)
*
* 규칙:
* · 단계는 자기 출력만 돌려줌 — 입력 · 다른 단계 출력을 고치지 않음(ctx 의 값은 읽기만).
@@ -31,8 +31,6 @@
* | profile | sub1 | planline · input.alignment | ProfileAlignment |
* | cross | sub2 | profile · ground · input.cross · pipes · structures | CrossOutput |
* | footprint | sub6 | cross · ground | PlanFootprint |
* | masshaul | sub2 | cross | unknown(sub2 모양) |
* | haul | sub2 | masshaul | unknown(서버 응답) |
* | drainage | sub3 | route · input.pipes | 「낡음」 표시(출력 안 씀) |
* | corridor | sub5 | profile · cross | 「낡음」 표시(출력 안 씀) |
*
@@ -238,8 +236,6 @@ export interface PipelineOutputs {
profile: ProfileAlignment;
cross: CrossOutput;
footprint: PlanFootprint;
masshaul: unknown;
haul: unknown;
drainage: null;
corridor: null;
}
+1 -21
View File
@@ -12,8 +12,6 @@
import { API_BASE_URL } from "@config/config_frontend";
import type { HaulPlan } from "@util/common_util_mass_haul_balance";
import type { StageContext } from "../A00_Common/design/logic/design_pipeline_types";
import type { MassHaulStageOutput } from "./logic/B05_Profile_MassHaul_Stage";
/** 서버가 돌려주는 배분 한 벌 — **화면이 쓰는 꼴 그대로**라 그리기 코드가 손대지 않는다.
* `import type` 이라 배분 모듈이 번들에 실리지 않는다(빌드에서 지워진다). */
@@ -26,7 +24,7 @@ const TIMEOUT_MS = 20000;
/** 열쇠별로 받아 둔 배분 — 되돌리기 · 보기 오감처럼 같은 누가토량이 다시 오면 묻지 않음(342-1). */
const KEPT_PLANS = 8;
/** 받은 배분 — `프로젝트:노선|열쇠` → 배분 · 오래된 것부터 버림 · 선반입 · 묶음 단계가 함께 씀. */
/** 받은 배분 — `프로젝트:노선|열쇠` → 배분 · 오래된 것부터 버림 · 선반입이 씀. */
const kept = new Map<string, HaulPlanPayload>();
/**
@@ -86,24 +84,6 @@ async function postHaulPlan(
}
}
/**
* 계산 묶음 `haul` 단계(PLAN 346-2 · 계약 `design_pipeline_types.ts`) — masshaul 의 횡단 기준 결과를 서버
* Node 로 배분 · 같은 열쇠면 안 물음 · 새 조작이 오면 `ctx.signal` 로 끊김(옛 번호 결과는 묶음이 버림).
* 못 받으면 앞 배분(`ctx.prev`) 그대로.
*/
export function createHaulStage(projectId: string, routeId: () => number | undefined) {
return async (ctx: StageContext<"haul">): Promise<HaulPlanPayload> => {
const result = (ctx.out.masshaul as MassHaulStageOutput | null)?.haulInput;
const route = routeId();
if (!result || !route) return (ctx.prev as HaulPlanPayload) ?? null;
const key = keyOf(result);
const at = keptKey(projectId, route, key);
if (kept.has(at)) return kept.get(at) ?? null;
const plan = await postHaulPlan(projectId, route, result, key, ctx.signal);
return plan === undefined ? ((ctx.prev as HaulPlanPayload) ?? null) : plan;
};
}
export interface HaulPlanPrefetch {
/** 새 누가토량 결과가 나왔음을 알린다 — 잠잠해지면 서버에 물어본다. */
schedule: (result: unknown) => void;
@@ -17,7 +17,7 @@ import { useDesignStore } from "../A00_Common/design/design_store";
import type { DesignPipeline, PipelineEvent } from "../A00_Common/design/logic/design_pipeline";
/** cross 를 지났다고 볼 단계(cross 가 같은 입력이라 건너뛰어도 뒤 단계가 알림). */
const AFTER_CROSS = new Set(["cross", "footprint", "masshaul"]);
const AFTER_CROSS = new Set(["cross", "footprint"]);
/** 묶음 차례가 cross 를 못 낸 채 끝나도(단계 실패) 「다시 계산 중」을 푸는 때(ms). */
const DRAFT_WAIT_MS = 3000;
@@ -1,53 +0,0 @@
/* =============================================================================
* B05_Profile_MassHaul_Stage.ts
* 계산 묶음 `masshaul` 단계(PLAN 346-2 · 계약 `design_pipeline_types.ts`) — cross(측점 열쇠마다 면적) ·
* profile → 유토곡선 계열(`computeMassHaulSeries` · 화면 유토곡선 · B06 과 같은 계산).
*
* 출력 = `MassHaulStageOutput`(계열 + 흙 배분에 보낼 횡단 기준 결과) · 순수 계산(DOM · fetch · 세션 없음).
* 토량환산계수 · 자연 사토 기울기는 부르는 쪽이 넘김(`context`) — 없으면 null.
* ========================================================================== */
import { computeMassHaulSeries, type MassHaulSeries } from "@util/common_util_mass_haul";
import type { EarthworkConversion, MassHaulSection } from "@util/common_util_mass_haul_types";
import type { StageContext } from "../../A00_Common/design/logic/design_pipeline_types";
export interface MassHaulStageContext {
conversion: EarthworkConversion;
naturalSpoilMinSlope?: number;
}
export interface MassHaulStageOutput {
series: MassHaulSeries[];
/** 흙 배분(haul 단계)에 보낼 결과 — 횡단 기준(없으면 첫 계열) · 계열이 없으면 null. */
haulInput: MassHaulSeries["result"] | null;
}
/** `masshaul` 단계 만들기 — `context()` 는 부를 때마다 읽음(설정이 바뀌면 다음 계산부터). */
export function createMassHaulStage(context: () => MassHaulStageContext | null) {
return (ctx: StageContext<"masshaul">): MassHaulStageOutput | null => {
const cross = ctx.out.cross;
const settings = context();
if (!cross || !settings) return null;
const sections: MassHaulSection[] = Object.values(cross.byKey)
.sort((a, b) => a.chainageM - b.chainageM)
.map((entry) => ({
station_id: entry.key,
chainage_m: entry.chainageM,
design: entry.detail as MassHaulSection["design"],
}));
const samples = ctx.out.profile?.samples ?? [];
const longitudinal = {
length_m:
samples[samples.length - 1]?.chainage_m ?? sections[sections.length - 1]?.chainage_m ?? 0,
design_profiles: samples.length ? [{ samples }] : [],
};
const series = computeMassHaulSeries(
longitudinal,
sections,
settings.conversion,
settings.naturalSpoilMinSlope,
);
const picked = series.find((entry) => entry.basis === "cross") ?? series[0];
return { series, haulInput: picked?.result ?? null };
};
}
@@ -4,7 +4,7 @@
*
* 추천 노선 맞춤(`B05_Profile_Engine_RouteInitial_Station.py` `_try` · `_solve`)을 줄인 꼴:
* · `from`(`fitStart` = 고친 자리 앞)부터 끝까지 차례로 — 고정 아닌 교각점(IP)을 들어오는 접선 따라 ±간격 안에서
* 밀어 MC(BC · EC 누가거리 가운데)를 가까운 일반측점(앞 MC 측점 + 간격 이상 · No.1 이상)에 둠.
* 밀어 MC(BC · EC 누가거리 가운데)를 일반측점(앞 MC 측점 + 간격 이상 · No.1 이상)에 둠 — 내림 · 올림 후보 중 뒤 곡선도 서는 쪽(454).
* · 곡선 하한을 새로 깨거나(`shortfallCrossed`) 앞 곡선 MC 를 흔들면 버림 → 그 곡선은 그대로.
* · 반지름 · 켬끔은 안 바꿈 — IP 자리만. 합치기 · 곡선 끄기 · 되돌아가기 없음.
* · 간격 = 스냅 간격(390-2 · 0.1 · 1 · 10 m · 측점 No.) · 허용 오차도 간격 따라(`mcTolM`).
@@ -139,7 +139,9 @@ function solve(
return null;
}
/** `from` 꺾임점부터 끝까지 곡선 MC 를 일반측점에 — 새 IP 목록(못 놓은 곡선 IP 는 그대로). */
/** `from` 꺾임점부터 끝까지 곡선 MC 를 일반측점에 — 새 IP 목록(못 놓은 곡선 IP 는 그대로).
* 곡선마다 후보 측점 둘(내림 · 올림 · 가까운 차례) · 뒤 곡선 하나를 미리 맞춰 보고 둘 다 서는 쪽(433-21 ②)
* — 가까운 쪽만 고르면 뒤 곡선이 「앞 측점 + 1」에 몰려 R 이 눌리고 앞 MC 가 흔들려 버려짐. */
export function fitCurveStations(
cols: Columns & Pick<NodeColumns, "planned">,
limits: FitLimits,
@@ -150,28 +152,64 @@ export function fitCurveStations(
if (!(interval > 0) || planned.length < 3) return planned;
let built = build(cols, planned, limits);
const floor = curveShortfalls(built.nodes, limits.limitRadiusM, limits.limitArcM);
const tol = mcTolM(interval);
type Fit = { planned: Vertex[]; built: EditedPolyline };
/** 곡선을 놓을 측점 — 내림 · 올림 중 앞 측점 + 1 이상 · 가까운 차례. */
const near = (mc: number, last: number): number[] => {
const lo = Math.floor(mc / interval);
return [lo, lo + 1]
.filter((n) => n > last)
.sort((a, b) => Math.abs(a * interval - mc) - Math.abs(b * interval - mc));
};
const onAt = (mc: number, last: number) =>
near(mc, last).find((n) => Math.abs(mc - n * interval) <= tol);
/** 곡선 `k` 를 측점 `n` 에 — 하한을 새로 깨거나 앞 곡선(`previous`) MC 를 흔들면 null. */
const attempt = (at: Fit, k: number, mc: number, n: number, previous: number): Fit | null => {
const fitted = solve(cols, limits, at.planned, k, n * interval, mc);
if (
!fitted ||
shortfallCrossed(
floor,
curveShortfalls(fitted.built.nodes, limits.limitRadiusM, limits.limitArcM),
)
)
return null;
if (previous < 0) return fitted;
const moved = Math.abs(
(mcOf(fitted.built, previous) ?? Infinity) - (mcOf(at.built, previous) ?? 0),
);
return moved <= tol / 5 ? fitted : null;
};
/** `k` 뒤 첫 곡선이 측점 위이거나 맞춰지나(앞보기 한 걸음). */
const nextSettles = (at: Fit, k: number, last: number): boolean => {
for (let j = k + 1; j < at.planned.length - 1; j += 1) {
const mc = mcOf(at.built, j);
if (mc === null) continue;
if (onAt(mc, last) !== undefined) return true;
return !cols.apexLock[j] && near(mc, last).some((n) => attempt(at, j, mc, n, k) !== null);
}
return true;
};
let lastStation = 0; // 앞 곡선이 놓인 측점 번호(없으면 0 = 시점)
let previous = -1; // 앞 곡선 꺾임점
for (let k = 1; k < planned.length - 1; k += 1) {
const mc = mcOf(built, k);
if (mc === null) continue;
const near = Math.max(lastStation + 1, Math.round(mc / interval));
const onStation = Math.abs(mc - near * interval) <= mcTolM(interval);
const fitted =
onStation || k < from || cols.apexLock[k] || Math.abs(near * interval - mc) > interval / 2
? null
: solve(cols, limits, planned, k, near * interval, mc);
const keep =
fitted &&
!shortfallCrossed(
floor,
curveShortfalls(fitted.built.nodes, limits.limitRadiusM, limits.limitArcM),
) &&
(previous < 0 ||
Math.abs((mcOf(fitted.built, previous) ?? Infinity) - (mcOf(built, previous) ?? 0)) <=
mcTolM(interval) / 5);
if (fitted && keep) ({ planned, built } = fitted);
if (onStation || keep) lastStation = near;
const on = onAt(mc, lastStation);
if (on !== undefined) lastStation = on;
else if (k >= from && !cols.apexLock[k]) {
let pick: { fit: Fit; n: number } | null = null;
for (const n of near(mc, lastStation)) {
const fit = attempt({ planned, built }, k, mc, n, previous);
if (!fit) continue;
pick ??= { fit, n };
if (nextSettles(fit, k, n)) {
pick = { fit, n };
break;
}
}
if (pick) (({ planned, built } = pick.fit), (lastStation = pick.n));
}
previous = k;
}
return planned;
+1 -3
View File
@@ -1,5 +1,5 @@
/* 344-3 · 346-2 2파 시험 묶음 진입점 — 진짜 모듈을 vite ssr 로 한 벌 묶어 Node 헬퍼가 부름.
* 측점 열쇠(옛 초안 옮김 · 노선 밖 고침) · cross · masshaul · haul 단계를 화면 · 서버 없이 잰다.
* 측점 열쇠(옛 초안 옮김 · 노선 밖 고침) · cross 단계를 화면 · 서버 없이 잰다.
*/
export {
carryStationDrafts,
@@ -20,8 +20,6 @@ export {
readRockBoundarySession,
} from "../../B06_Section/B06_Section_UI_Page_Persist";
export { createCrossStage, groundSamples } from "../../B06_Section/logic/B06_Section_Cross_Stage";
export { createMassHaulStage } from "../../B05_Profile/logic/B05_Profile_MassHaul_Stage";
export { createHaulStage } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
export {
buildAlignment,
emptyEdits,
@@ -202,39 +202,4 @@ const edited = m.buildAlignment(base, { station_offsets: { "40.000": 0.4 }, curv
stage(ctxOf(touched, { sources: ["grade"], fromM: 20, toM: 60, keys: [] }, third, edited));
out.crossGrade = counts();
/* 6. masshaul 단계 — 계열 · 흙 배분 입력 */
const conversion = {
soil: { loose: 1.25, compacted: 0.9 },
ripping_rock: { loose: 1.5, compacted: 1.2 },
blasting_rock: { loose: 1.65, compacted: 1.35 },
};
const massStage = m.createMassHaulStage(() => ({ conversion }));
const mass = massStage({ ...ctxOf(crossInput, null, null), out: { profile, cross: first } });
out.massSeries = mass.series.map((s) => s.basis);
out.massHaulInput = Array.isArray(mass.haulInput?.points);
/* 7. haul 단계 — 같은 열쇠 안 물음 · 끊김 = 앞 배분 그대로 */
const posts = [];
globalThis.fetch = async (_url, init) => {
if (init.signal?.aborted) throw new Error("aborted");
posts.push(1);
return { ok: true, json: async () => ({ haul_plan: { n: posts.length } }) };
};
const haul = m.createHaulStage(P, () => R);
const haulCtx = (masshaul, prev, signal = new AbortController().signal) => ({
...ctxOf(crossInput, null, prev),
signal,
out: { masshaul },
});
const plan1 = await haul(haulCtx(mass, null));
const plan2 = await haul(haulCtx(mass, plan1));
const other = massStage({
...ctxOf(crossInput, null, null),
out: { profile: edited, cross: third },
});
const aborted = new AbortController();
aborted.abort();
const plan3 = await haul(haulCtx(other, plan1, aborted.signal));
out.haul = { posts: posts.length, plan1, plan2, plan3 };
process.stdout.write(JSON.stringify(out));
@@ -133,7 +133,7 @@ async function run() {
};
}
// ② 늦은 응답 버림 — 브라우저 단계(ground 늦음) · 뒤 단계(haul 늦음 · 끊김)
// ② 늦은 응답 버림 — 브라우저 단계(ground 늦음) · 뒤 단계(corridor 늦음 · 끊김)
{
const log = [];
let releaseGround = null;
@@ -161,25 +161,27 @@ async function run() {
commits,
};
// 뒤 단계 — haul 1 이 늦게 옴 · 그 사이 새 조작 → 끊김 · 버림
let releaseHaul = [];
const hold2 = { haul: () => new Promise((resolve) => releaseHaul.push(resolve)) };
// 뒤 단계 — corridor 1 이 늦게 옴 · 그 사이 새 조작 → 끊김 · 버림
let releaseAfter = [];
const hold2 = { corridor: () => new Promise((resolve) => releaseAfter.push(resolve)) };
const log2 = [];
const pipe2 = pipeMod.createDesignPipeline({ stages: fakeStages(log2, hold2) });
const commits2 = [];
pipe2.subscribe((e) => e.kind === "stage" && e.stage === "haul" && commits2.push(e.generation));
pipe2.subscribe(
(e) => e.kind === "stage" && e.stage === "corridor" && commits2.push(e.generation),
);
await pipe2.start(BASE);
const haul1 = log2.find((e) => e.name === "haul");
const after1 = log2.find((e) => e.name === "corridor");
await pipe2.update("cross", { x: 1 }, { fromM: 0, toM: 0 });
const haul1Aborted = haul1.aborted();
releaseHaul[0]();
const after1Aborted = after1.aborted();
releaseAfter[0]();
await tick(1);
releaseHaul[1]();
releaseAfter[1]();
await pipe2.settled();
out.lateAfter = {
haul1Aborted,
after1Aborted,
commits: commits2,
haulGen: pipe2.output("haul").gen,
afterGen: pipe2.output("corridor").gen,
generation: pipe2.generation,
};
}
@@ -1,4 +1,4 @@
/* PLAN 346-1 · 346-2 붙임 단위검증 헬퍼 — 페이지 묶음에 cross · masshaul · haul · drainage · corridor 를
/* PLAN 346-1 · 346-2 붙임 단위검증 헬퍼 — 페이지 묶음에 cross · drainage · corridor 를
* 붙인 것. 진짜 TS(`design_pipeline_page` · `_cross` · 단계 파일 전부)를 그 자리에서 트랜스파일해 Node 로
* 돌림. 서버 창구(노선 · 구조물 · 관 목록 · 지형 격자 · 표준단면 · 흙 배분 fetch)만 가짜.
* 입력 argv[2] JSON · argv[3] 격자 이진. 결과 = 표준출력 JSON. 짝 = `test_346_2_pipeline_cross_page.py`. */
@@ -6,6 +6,7 @@ const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const CATALOG = require("./helper_322_catalog_module.cjs");
const finalCumulative = require("./helper_454_final_cumulative.cjs");
const ROOT = path.join(__dirname, "..", "..");
const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
@@ -29,7 +30,7 @@ globalThis.window = {
clearTimeout,
};
// 흙 배분 서버 — 늦게 답함 · 끊기면 버림
// 흙 배분 서버 — 늦게 답함 · 끊기면 버림 · 묶음은 안 부름(454 masshaul · haul 단계 걷음)
const haul = { calls: 0, aborted: 0, answered: 0 };
globalThis.fetch = (url, init) =>
new Promise((resolve, reject) => {
@@ -205,7 +206,7 @@ async function run() {
stationIntervalM: job.interval,
onCorridorStale: () => (corridor += 1),
});
pageMod.pipelineEarthwork(P, { conversion: job.conversion });
pageMod.pipelineEarthwork(P);
const pipe = pageMod.pagePipeline(P);
const commits = [];
pipe.subscribe((e) => e.kind === "stage" && commits.push(`${e.stage}@${e.generation}`));
@@ -242,7 +243,6 @@ async function run() {
const ground = pipe.output("ground");
const profile = pipe.output("profile");
const shown = pageMod.pipelineDetail(P);
const mass = pipe.output("masshaul");
const moved = {
run: {
outcome: runB.outcome,
@@ -264,7 +264,7 @@ async function run() {
cut: pipe.output("cross").byKey[s.key]?.cutAreaM2 ?? null,
fill: pipe.output("cross").byKey[s.key]?.fillAreaM2 ?? null,
})),
finalCumulative: mass?.haulInput?.final_cumulative_m3 ?? null,
finalCumulative: finalCumulative(load, pipe, job.conversion),
footprint: pipe.output("footprint")?.records.length ?? 0,
detail: shown && {
sections: shown.cross_sections.length,
@@ -6,6 +6,7 @@
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const finalCumulative = require("./helper_454_final_cumulative.cjs");
const CATALOG = require("./helper_322_catalog_module.cjs");
const ROOT = path.join(__dirname, "..", "..");
@@ -167,7 +168,7 @@ async function run() {
drawn = next;
void Promise.resolve().then(renderB06);
});
const final = () => pipe.output("masshaul")?.haulInput?.final_cumulative_m3 ?? null;
const final = () => finalCumulative(load, pipe, job.conversion);
const cut = () =>
Object.values(pipe.output("cross")?.byKey ?? {}).reduce((s, c) => s + (c.cutAreaM2 ?? 0), 0);
@@ -5,6 +5,7 @@
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const finalCumulative = require("./helper_454_final_cumulative.cjs");
const CATALOG = require("./helper_322_catalog_module.cjs");
const ROOT = path.join(__dirname, "..", "..");
@@ -249,7 +250,7 @@ async function draftRun(P, grid) {
totalMs: run.totalMs,
line: pipe.output("route").line,
curves: pipe.output("route").curves,
final: pipe.output("masshaul")?.haulInput?.final_cumulative_m3 ?? null,
final: finalCumulative(load, pipe, job.conversion),
stations,
copied,
};
@@ -6,6 +6,7 @@
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const finalCumulative = require("./helper_454_final_cumulative.cjs");
const CATALOG = require("./helper_322_catalog_module.cjs");
const ROOT = path.join(__dirname, "..", "..");
@@ -198,7 +199,7 @@ async function run() {
stations: chainagesOf(stations),
saved: job.detail.cross_sections.map((s) => s.chainage_m),
pipeChainages: (pipe.input("pipes") ?? []).map((p) => p.chainage_m),
final: pipe.output("masshaul")?.haulInput?.final_cumulative_m3 ?? null,
final: finalCumulative(load, pipe, job.conversion),
footprint: pipe.output("footprint")?.records.length ?? 0,
};
@@ -239,7 +240,7 @@ async function run() {
const again = await openOnce("proj-349b");
const reopen = {
runs: runs().slice(again.before).length,
final: again.pipe.output("masshaul")?.haulInput?.final_cumulative_m3 ?? null,
final: finalCumulative(load, again.pipe, job.conversion),
stations: chainagesOf(again.pipe.output("ground")?.stations),
};
again.stop();
@@ -0,0 +1,96 @@
/* 454(433-21 ②) 방향키 측점 No. 걸음 — 무작위 짧은 다리 노선(14 IP · 고정 R) 교점마다 네 방향 한 걸음
* 누르고 측점 밖 막힘 수를 JSON 한 줄로(`test_454_arrow_station_step.py` 가 판정).
* 누름 판정은 편집기 방향키 길과 같음 — 하한 깸 = 막힘 · 미리보기(`fitDrag`) 측점 밖이 늘면 막힘.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) =>
request.startsWith(".")
? loadTs(path.join(path.dirname(full), `${request}.ts`))
: require(request);
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const LOGIC = path.join(ROOT, "B05_Profile", "logic");
const fit = loadTs(path.join(LOGIC, "B05_Profile_RouteEdit_StationFit.ts"));
const edits = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Edits.ts"));
const INTERVAL = 20;
const LIMITS = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, intervalM: INTERVAL };
const columns = (p) => ({
planned: p,
curveOn: p.map(() => true),
curveRadius: p.map(() => null),
curveLock: p.map(() => null),
curveArc: p.map(() => null),
apexLock: p.map((_, i) => i === 0 || i === p.length - 1),
});
const copy = (c) => ({
...c,
planned: c.planned.map(([x, y]) => [x, y]),
curveRadius: [...c.curveRadius],
});
const off = (b) => fit.offStationCount(b.vertices, b.curves, INTERVAL);
const floor = (b) => edits.curveShortfalls(b.nodes, LIMITS.limitRadiusM, LIMITS.limitArcM);
function route(seed) {
const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
const line = [[0, 0]];
let a = 0;
for (let i = 0; i < 14; i += 1) {
a += (rnd() - 0.5) * 1.6;
const length = 35 + rnd() * 60;
const p = line[line.length - 1];
line.push([p[0] + Math.cos(a) * length, p[1] + Math.sin(a) * length]);
}
return line;
}
const out = { press: 0, floorBlock: 0, offBlock: 0, offStart: 0, offAfterRelease: 0 };
for (let seed = 1; seed <= 10; seed += 1) {
const hist = columns(fit.fitCurveStations(columns(route(seed)), LIMITS, 1));
hist.curveRadius = edits
.rebuildLine(copy(hist), LIMITS)
.nodes.map((n) => (n.radius_m === null ? null : Math.round(n.radius_m * 100) / 100));
out.offStart += off(edits.rebuildLine(copy(hist), LIMITS));
for (let k = 1; k < hist.planned.length - 1; k += 1)
for (const [dx, dy] of [
[1, 0],
[-1, 0],
[0, 1],
[0, -1],
]) {
const st = copy(hist);
const b0 = edits.rebuildLine(st, LIMITS);
const node = st.planned[k];
st.planned[k] = [node[0] + dx * INTERVAL, node[1] + dy * INTERVAL];
out.press += 1;
if (edits.shortfallCrossed(floor(b0), floor(edits.rebuildLine(st, LIMITS)))) {
out.floorBlock += 1;
continue;
}
const pv = { ...st, planned: fit.fitDrag(st, hist.planned, k, LIMITS) };
if (off(edits.rebuildLine(copy(pv), LIMITS)) > off(b0)) {
out.offBlock += 1;
continue;
}
const rel = { ...pv, planned: fit.fitCurveStations(pv, LIMITS, fit.fitStart(hist, pv)) };
if (off(edits.rebuildLine(copy(rel), LIMITS))) out.offAfterRelease += 1;
}
}
process.stdout.write(JSON.stringify(out));
@@ -0,0 +1,23 @@
/* 454 — 옛 masshaul 단계 탐침 자리: 묶음 cross · profile 출력으로 누가토량 끝값(m3).
* 유토곡선 판(`B05_Profile_UI_Profile_MassHaul`)과 같은 공용 계산(`computeMassHaulSeries`) · 횡단 기준.
* `load` = 부르는 헬퍼의 TS 로더(`load("common_util", "…ts")`). */
module.exports = function finalCumulative(load, pipe, conversion) {
const cross = pipe.output("cross");
const samples = pipe.output("profile")?.samples ?? [];
if (!cross) return null;
const { computeMassHaulSeries } = load("common_util", "common_util_mass_haul.ts");
const sections = Object.values(cross.byKey)
.sort((a, b) => a.chainageM - b.chainageM)
.map((e) => ({ station_id: e.key, chainage_m: e.chainageM, design: e.detail }));
const series = computeMassHaulSeries(
{
length_m:
samples[samples.length - 1]?.chainage_m ?? sections[sections.length - 1]?.chainage_m ?? 0,
design_profiles: samples.length ? [{ samples }] : [],
},
sections,
conversion,
);
const picked = series.find((s) => s.basis === "cross") ?? series[0];
return picked?.result?.final_cumulative_m3 ?? null;
};
+1 -8
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""344-3 · 346-2 2파 — 세션 횡단 초안 측점 열쇠 · cross · masshaul · haul 단계.
"""344-3 · 346-2 2파 — 세션 횡단 초안 측점 열쇠 · cross 단계(masshaul · haul 단계는 454 에서 걷음).
못박는 것
- 옛 초안(누가거리 열쇠)은 읽을 때 측점 열쇠로 옮김 · 지금 측점에 없는 것은 지우지 않고 남김.
@@ -8,7 +8,6 @@
- 노선이 바뀜 = 측점 열쇠로 넘김(관 · 구조물은 옮긴 자리로) · 넘김 = 카드 선택 넷 · 암 경계 ·
노견 확장(417) · 단면 유형 · 측구 방향 · 포장은 안 넘김 · 사라진 측점 = 「노선 밖 고침 N 건」.
- cross 단계: 같은 입력 다시 셈 0 · 측점 하나 고침 = 1 · 범위 밖 재사용.
- masshaul 단계 = 계열 + 흙 배분 입력 · haul 단계 = 같은 열쇠 안 물음 · 끊김 = 앞 배분 그대로.
Node 헬퍼 `helper_344_3_station_keys.mjs` 가 vite ssr 로 묶은 진짜 모듈(`helper_344_3_entry.ts`)을
부름.
@@ -116,9 +115,3 @@ def test_cross_단계_국부_연산(out):
assert out["crossOne"] == {"computed": 1, "reused": 5}
assert out["crossOneChanged"] == "soil"
assert out["crossGrade"]["computed"] <= 3 and out["crossGrade"]["reused"] >= 3
def test_masshaul_haul_단계(out):
assert out["massSeries"] == ["cross", "longitudinal"]
assert out["massHaulInput"] is True
assert out["haul"] == {"posts": 1, "plan1": {"n": 1}, "plan2": {"n": 1}, "plan3": {"n": 1}}
@@ -31,9 +31,8 @@ BROWSER = [
"profile",
"cross",
"footprint",
"masshaul",
]
AFTER = ["haul", "drainage", "corridor"]
AFTER = ["drainage", "corridor"]
@pytest.fixture(scope="module")
@@ -65,7 +64,7 @@ def test_같은_입력_다시_안_셈(r):
b = r["basic"]
assert b["same"] == {"result": None, "generation": 1} # 같은 값 = 차례 번호도 안 오름
# 횡단만 바뀌면 노선 ~ 종단 · 배수유역은 건너뜀
assert b["crossRan"] == ["cross", "footprint", "masshaul", "haul", "corridor"]
assert b["crossRan"] == ["cross", "footprint", "corridor"]
assert b["crossSkipped"] == [
"route",
"follow",
@@ -76,7 +75,7 @@ def test_같은_입력_다시_안_셈(r):
"drainage",
]
# 계획고만 바뀌면 측점 · 지반 · 첫 계획선 · 배수유역 건너뜀
assert b["gradeRan"] == ["profile", "cross", "footprint", "masshaul", "haul", "corridor"]
assert b["gradeRan"] == ["profile", "cross", "footprint", "corridor"]
# 노선이 바뀌면 전부
assert b["routeRan"] == BROWSER + AFTER
@@ -101,8 +100,8 @@ def test_늦은_응답_버림_브라우저_단계(r):
def test_늦은_응답_버림_뒤_단계_끊김(r):
late = r["lateAfter"]
assert late["haul1Aborted"] is True # 새 조작 = 옛 요청 끊음
assert late["commits"] == [2] and late["haulGen"] == 2 and late["generation"] == 2
assert late["after1Aborted"] is True # 새 조작 = 옛 요청 끊음
assert late["commits"] == [2] and late["afterGen"] == 2 and late["generation"] == 2
def test_못_끝낸_단계는_범위를_합쳐_셈(r):
@@ -133,8 +132,6 @@ def test_단계_시간_기록(r):
"profile",
"cross",
"footprint",
"masshaul",
"haul",
"corridor",
]
assert t["last"]["totalIsNumber"] is True
@@ -201,7 +201,7 @@ def r(request) -> dict:
def test_열_때_한_번_발자국_저장_횡단_입력(r):
o = r["open"]
assert o["outcome"] == "done"
assert {"cross", "footprint", "masshaul"} <= set(o["stages"])
assert {"cross", "footprint"} <= set(o["stages"])
assert o["crossCount"] == o["stations"] and o["footprint"] > 2
assert o["carried"] == o["stations"] # 저장 측점마다 입력이 측점 열쇠로
assert o["corridor"] == 0 and o["haulCalls"] == 0 and o["detail"] is None
@@ -214,7 +214,7 @@ def test_노선_옮김_한_번에_유토곡선까지_시간(r):
assert run["outcome"] == "done" and run["sources"] == ["route"]
for stage in ("route", "stations", "ground", "planline", "profile", "cross", "footprint"):
assert stage in run["stages"], stage
assert "masshaul" in run["stages"]
assert "masshaul" not in run["stages"] and "haul" not in run["stages"] # 454 걷음
assert (
sum(run["stages"][s] for s in run["stages"] if s not in ("haul", "drainage", "corridor"))
< BUDGET_MS
@@ -271,9 +271,8 @@ def test_누가토량_서버_엔진과_같은_자리수(r):
def test_늦은_응답_버림(r):
late = r["late"]
assert late["genB"] > late["genA"]
assert late["haul"]["aborted"] >= 1 # 앞 차례 흙 배분 요청 끊김
assert f"haul@{late['genA']}" not in late["haulCommits"] # 옛 차례 배분 안 들어옴
assert late["haulCommits"][-1] == f"haul@{late['genB']}"
# 454 — 묶음은 흙 배분을 안 물음(유토곡선 판 선반입만) · 늦은 응답 버림 = 344-4 corridor
assert late["haul"]["calls"] == 0 and late["haulCommits"] == []
def test_저장_관_목록이_좌표로_따라감(r):
@@ -3,7 +3,7 @@
- 그리기 = 횡단 기준만(`["cross"]`) · 범례는 낸 계열마다라 종단 단추가 안 섬.
- 저장된 범례가 종단(`longitudinal`)이면 횡단으로 맞춤 · 분배 켬은 그대로.
- 계산 묶음 단계(MassHaul_Stage)는 두 기준 그대로 · 공용 계산 코드 남김.
- 공용 계산 코드는 두 기준 그대로 남김(계산 묶음 masshaul 단계는 454 에서 걷음).
`helper_354_2_masshaul_cross_only.cjs` 가 진짜 TS 를 잘라 돌림.
"""
@@ -52,8 +52,7 @@ def test_그리기는_횡단만_부름():
def test_묶음_단계와_공용_코드는_그대로():
stage = (ROOT / "B05_Profile/logic/B05_Profile_MassHaul_Stage.ts").read_text(encoding="utf-8")
assert '["cross"]' not in stage
assert not (ROOT / "B05_Profile/logic/B05_Profile_MassHaul_Stage.ts").exists() # 454 걷음
util = (ROOT / "common_util/common_util_mass_haul.ts").read_text(encoding="utf-8")
assert 'const SERIES_BASES: MassHaulBasis[] = ["cross", "longitudinal"];' in util
assert "longitudinalAreaSamples(longitudinal, sections)" in util
@@ -0,0 +1,35 @@
"""454(433-21 ②) 방향키 측점 No. 20 m 걸음 막힘 — 곡선 맞춤이 후보 측점 둘(내림 · 올림) + 뒤 곡선
앞보기로 고름. 무작위 노선 열 개 · 교점마다 네 방향 = 520 누름(`helper_454_arrow_station_step.cjs`).
고치기 전 측점 밖 막힘 48 · 처음 맞춤 측점 밖 2 · 놓은 뒤 측점 밖 43 → 6 · 0 · 0.
./venv/Scripts/python.exe -m pytest resources/tester/test_454_arrow_station_step.py -q
"""
import json
import subprocess
from pathlib import Path
import pytest
HELPER = Path(__file__).with_name("helper_454_arrow_station_step.cjs")
@pytest.fixture(scope="module")
def got() -> dict:
run = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=120
)
assert run.returncode == 0, run.stderr
return json.loads(run.stdout)
def test_누름_수(got) -> None:
assert got["press"] == 520
def test_측점_밖_막힘_줄어듦(got) -> None:
assert got["offBlock"] <= 10
def test_처음_맞춤_놓은_뒤_측점_밖_없음(got) -> None:
assert got["offStart"] == 0 and got["offAfterRelease"] == 0