refactor(B05·B06): 소단 = 옹벽처럼 측점에 제원 얹음 — 형상 얹기 한 벌 · 세션 사본 berm 걷음 (PLAN 577-2 · 요구 434 · 437)

- 얹기: attachShapes · attach_shapes 에 소단(section.berm · 덮는 소단 없음 = null) · SHAPE_KEYS 에 berm · TS attachBermSpecs ↔ 파이썬 attach_berm_specs · berm_spans_from 짝
- 읽기: 저장 노선 다시 셈(브라우저 · Node cross.refresh) = 얹힌 section.berm(칸 없음 = 저장분 design.berm) · 노선 초안 · 서버 Node 묶음 cross = crossWithShapes 의 얹힌 제원(bermAt · crossWithBerm 걷음)
- 계단은 계산 안쪽 그대로 · [저장] 길(구조물 정본 → cross.berm_spans) 그대로
- 지움: 시설 줄 syncBerm · putBerm(C4.copy) · writeBermSpans · 세션 칸 berm · bermRequest · design_shared bermAt
- 시험: test_272_berm_source 측점 얹기 거울로 · test_323_4 소단 얹음 · 477 · 511-A · 472 떠남 · 365 · 272 고름 헬퍼가 사본 대신 얹힌 소단 · design_pipeline_page 700 줄 → 699(주석 한 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 11:33:06 +09:00
co-authored by Claude Opus 5.5
parent 189474a507
commit 403722e687
33 changed files with 249 additions and 308 deletions
-3
View File
@@ -235,9 +235,6 @@ export const STATE_REGISTRY = {
"design-stations": { bucket: "result", scope: "project" },
"design-profile": { bucket: "result", scope: "project" },
"design-footprint": { bucket: "result", scope: "project" },
/** 소단 사본 — 구조물(소단)에서 셈한 구간 목록(`design_facility_lane` 만 씀 · 고침은 `structures` 칸).
* 다시 셈에 실어 보냄 · 안 실으면 계획선을 고치는 순간 계단이 사라짐(3-9 · 477-4). */
berm: { bucket: "result", scope: "route" },
} as const satisfies Record<string, StateEntry>;
export type StateName = keyof typeof STATE_REGISTRY;
+1 -7
View File
@@ -142,18 +142,12 @@ export interface CalcRequest {
pick: (results: Record<string, unknown>) => unknown;
}
/** F7 구조물 면적 · C4 소단 구간 — 등록부 `node:` 입력 그대로. */
/** F7 구조물 면적 — 등록부 `node:` 입력 그대로. */
export const areasRequest = (rows: unknown[]): CalcRequest => ({
run: ["structure.layouts"],
inputs: { detail: { cross_sections: rows } },
pick: (results) => (results["structure.layouts"] as { areas: unknown }).areas,
});
export const bermRequest = (structures: unknown[]): CalcRequest => ({
run: ["cross.berm_spans"],
inputs: { berm: { structures, chainages: [] } },
pick: (results) => (results["cross.berm_spans"] as { spans: unknown }).spans,
});
export const trialRequest = (id: string, args: unknown[]): CalcRequest => ({
run: [id],
inputs: { args: { [id]: args } },
+1 -43
View File
@@ -8,20 +8,10 @@
* 입력 = 관 알림(배수유역도 · B06 관 목록) · 구조물(보기 고침 · 저장분) · 종류 표 · 측점 간격 · 추가 측점(가게)
* 관 id 장부 = 관 고정 id 가 쥔 세션 id 를 그 자리로(411-B) · 헛 id 정리(411-E) — 어느 보기에서 고쳐도
* 계획고 편집 옮김(사라진 측점 → 새 측점 · 213-2) = 사용자 고침만 · 노선 따라감(`follow`)은 안 옮김(계산 묶음 몫 · 349-0d)
* 소단 사본(세션 `berm`) = 구조물 소단 구간 — 쓰는 자리 여기 하나(B05 만 열어도 · 같으면 안 씀 · 쓰면 cross 한 번 · 467)
* ========================================================================== */
import { useDesignStore } from "./design_store";
import { pageRouteId } from "./design_page_route";
import { readState } from "../b_page_state";
import {
bermSpansFromStructures,
writeBermSpans,
type BermSpan,
} from "../../B06_Section/B06_Section_UI_Page_Persist";
import { pipeIdAt, placePipeIds, prunePipeIds, withPipeIds } from "./design_pipe_ids";
import { calcOnServerNow } from "./design_calc_sites";
import { bermRequest, calcChecked, calcOnServerOnly } from "./design_calc_server";
import { plannedIrregular } from "./design_structure_status";
import { planStations } from "../../common_util/common_util_structure_station";
import { followPipeReports } from "../../B05_Profile/logic/B05_Profile_Pipeline_Follow";
@@ -99,7 +89,7 @@ export function createFacilityLane(
let types: StructureType[] = [];
let stored: StructureInstance[] | null = null;
let structures: StructureInstance[] = [];
let structuresKnown = false; // 구조물을 받기 전 = 소단 사본 안 씀(빈 목록으로 저장분 소단을 지우지 않게)
let structuresKnown = false; // 구조물을 받기 전 = 측점 계획 모름(빈 목록으로 저장 시설 측점을 지우지 않게)
/** 마지막 관 알림 — 노선 따라감 · 관 초안이 바뀌면 이것을 초안 측점으로 다시 맞춤. */
let reported: LanePipe[] | null = null;
let pipes: LanePipe[] = [];
@@ -152,36 +142,6 @@ export function createFacilityLane(
const marks = (): StructureInstance[] => [...structures, ...pipesToStructureMarks(pipes)];
/** 소단 사본 = 지금 구조물의 소단 구간(467 · 사본 없는 동안 다시 셈 = 저장분 `design.berm`) · 같으면 안 씀 ·
* 쓰면 저장 노선 cross 한 번 더(같은 값 측점은 다시 셈 기억이 건너뜀 · 묶음은 늦게 불러옴 · 순환 없음). */
function syncBerm(): void {
const routeId = pageRouteId(projectId);
if (!structuresKnown || routeId === null) return;
// 계산 자리 quantity = server 면 잠잠 뒤 서버 값만(511-A) · 아니면 브라우저 셈
if (calcOnServerNow("quantity")) {
void calcOnServerOnly<BermSpan[]>("quantity", "C4.copy", bermRequest(structures)).then(
(next) => next && putBerm(routeId, next),
);
return;
}
putBerm(
routeId,
calcChecked("quantity", "cross.berm_spans", bermSpansFromStructures, [structures]),
);
}
function putBerm(routeId: number, next: BermSpan[]): void {
const copy = readState<BermSpan[]>("berm", projectId, routeId);
if (Array.isArray(copy) && JSON.stringify(copy) === JSON.stringify(next)) return;
writeBermSpans(projectId, routeId, next);
void Promise.all([import("./design_pipeline_cross"), import("./design_pipeline_page")])
.then(([cross, page]) => {
cross.forceCross();
page.pipelineCross(projectId);
})
.catch(() => undefined);
}
/** 측점 계획 입력이 다 옴 — 모름(관 알림 전 · 구조물 받기 전)을 빈 목록으로 셈하면 종단도가 저장 시설 측점
* (관 등)을 뺀 선으로 횡단을 다시 셈 · 유토 배지가 잠깐 틀림(473 · 요구 272). */
const known = (): boolean => types.length > 0 && reported !== null && structuresKnown;
@@ -234,14 +194,12 @@ export function createFacilityLane(
}
if (next.pipes) reported = [...next.pipes];
refresh(carryEdits);
if (next.structures) run(syncBerm);
},
follow() {
const drafted = store.get("structures");
structures = drafted ?? stored ?? [];
structuresKnown ||= drafted !== null || stored !== null;
refresh(false);
run(syncBerm);
},
setIntervalM(next) {
if (next > 0 && next !== intervalM) {
+2 -3
View File
@@ -338,14 +338,13 @@ function createPagePipeline(
groundPvi: groundPviAt, // 계산 자리 「서버」 = 서버 Node(511-B B-3)
}),
profile: profileAt,
// 단면 유형 = 새 지형 상단측 · 소단 = 지금 구조물(365-5) · 저장 노선 = 다시 셈 창구 한 벌(365-4) · 엔진은
// 창구 출력을 앞 결과로 안 이어 씀(초안 = 새로고침)
// 단면 유형 = 상단측 · 소단 = 얹힌 제원(577-2) · 저장 노선 = 다시 셈 창구(365-4) · 엔진은 창구 출력 안 이어 씀
cross: (ctx) => {
const stations = ctx.out.ground?.stations ?? [];
const status = statusOf(ctx.inputs.pipes, ctx.inputs.structures);
const engine = () =>
crossStage.stage({
...contextCross(ctx, status, options.bermAt?.(ctx.inputs.structures) ?? null),
...contextCross(ctx, status),
prev: ctx.prev && savedOut.has(ctx.prev) ? null : ctx.prev,
});
// 계산 자리 cross = server — 엔진 길도 서버 Node 단계(같은 TS · 511-B B-4) · 못 받음 = 브라우저
@@ -14,7 +14,6 @@ import type {
AlignmentBase,
AlignmentEdits,
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
import type { BermAt } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
import type { DesignStoreValues } from "./design_store";
@@ -61,8 +60,6 @@ export interface PagePipelineOptions {
stationKeys?: (routeId: number, detail: SectionDetailResponse) => string[];
/** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */
onUnstable?: () => void;
/** 소단 구간(지금 구조물 목록 → 누가거리마다 제원) — 초안 cross 가 따라간 구조물로 소단을 셈(365-5). */
bermAt?: (structures: unknown) => BermAt;
/** 저장 노선 cross = 다시 셈 창구 한 벌(365-4 · 측점이 상세와 다르면 null = 엔진) · 주면 종단도는 따로 안 셈. */
savedCross?: (
d: SectionDetailResponse,
+1 -11
View File
@@ -59,8 +59,7 @@ import {
refreshCrossDesigns,
type CrossRefreshInput,
} from "../../B06_Section/B06_Section_Cross_Refresh";
import { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist";
import { areasRequest, bermRequest, calcChecked } from "./design_calc_server";
import { areasRequest, calcChecked } from "./design_calc_server";
import { registeredKeys, stationIntervalOf } from "../../B06_Section/B06_Section_Station_Keys";
import { detailCross } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
import type { CrossOutput, PipelineStation } from "./logic/design_pipeline_types";
@@ -255,15 +254,6 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
each((hooks) => hooks.onFacilitiesFollowed?.(far));
},
onUnstable: () => showToast(L("Design_Pipeline_Unstable"), "warning"),
// 초안 소단 = 지금 구조물(따라간 자리)의 소단 구간(365-5 · 저장 노선은 다시 셈 창구가 세션 사본으로)
bermAt: (structures) => {
const list = Array.isArray(structures) ? structures : [];
// 계산 자리 quantity = server 면 잠잠 뒤 서버 대조만(초안 미리보기 · 511-A)
const spans = calcChecked("quantity", "cross.berm_spans", bermSpansFromStructures, [list], {
request: bermRequest(list),
});
return (chainageM) => bermSpecAt(spans, chainageM);
},
// 저장 노선 횡단 = 다시 셈 창구 한 벌(365-4) — 카드 · 유토곡선 · [저장] 패치가 보는 상세 설계를 늘 고치고 그
// 값을 묶음 출력으로(발자국 · 평면 횡단 판) · 측점이 상세와 다르면(갈아끼움 전) 묶음 엔진(439)
savedCross: (detail, routeId, stations) =>
@@ -18,7 +18,6 @@
* ========================================================================== */
import { readFileSync, statSync } from "node:fs";
import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans";
import type { FollowingStructure } from "@util/common_util_route_follow";
import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog";
import type { PlanPipe, PlanStructure, PlanType } from "@util/common_util_structure_station";
@@ -189,9 +188,7 @@ async function runStages(
ctx.inputs.structures ?? savedStructures,
types,
);
const list = Array.isArray(ctx.inputs.structures) ? ctx.inputs.structures : [];
const spans = bermSpansFromStructures(list);
return crossStage.stage(contextCross(ctx, status, (at) => bermSpecAt(spans, at)));
return crossStage.stage(contextCross(ctx, status));
},
};
if (stages.until === "follow") return followOnly(table, inputs);
@@ -10,7 +10,6 @@ import {
} from "../../../common_util/common_util_route_follow";
import {
crossWithContext,
type BermAt,
type CrossInput,
type ShapeStatus,
} from "../../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
@@ -45,11 +44,10 @@ export function placedFollowInputs(
};
}
/** cross 단계 입력 = 시설 맥락(새 자리 그림) · 단면 유형 기본값(상단측) · 소단(지금 구조물) 얹음(365-5 · 457). */
/** cross 단계 입력 = 시설 맥락(새 자리 그림 · 소단 = 얹힌 제원 · 577-2) · 단면 유형 기본값(상단측) 얹음(365-5 · 457). */
export const contextCross = (
ctx: StageContext<"cross">,
status: ShapeStatus | null,
bermAt: BermAt | null,
): StageContext<"cross"> => ({
...ctx,
inputs: {
@@ -58,7 +56,6 @@ export const contextCross = (
ctx.inputs.cross as CrossInput | null,
ctx.out.ground?.stations ?? [],
status,
bermAt,
),
},
});
@@ -8,7 +8,8 @@
* `resources/tester/test_346_2_pipeline_cross_page.py`.
* crossWithUphill cross 단계 입력에 단면 유형 기본값(상단측)을 얹음 — 서버 노선 저장이 단면 유형 ·
* 측구 방향을 새 지형으로 정하는 것과 같은 규칙(넘기는 칸 = 부르는 쪽 입력).
* crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457).
* crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강 · 소단) = 지금 현황을 새 측점에 다시 얹은 그림에서(457 ·
* 소단 = 얹힌 `berm` 제원 + 세션 쪽 깊이 · 577-2).
* crossAtSeat 자리 칸(암 경계 · 2단 경사 · 손 상단측 · `seats`)은 측점이 그 자리 그대로일 때만 · 새 자리 =
* 기본값(요구 275-3 · 판정 `sameSeat` · 서버 노선 저장 · Node 넘김과 같은 규칙).
* draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 그림 자료 · 카드 이름 = 지금
@@ -113,38 +114,6 @@ export function crossWithUphill(
return { ...input, byKey };
}
/** 소단 한 측점 제원(세션 `berm` 꼴 · 구조물 목록에서 폄). */
export type BermAt = (
chainageM: number,
) => { width_m: number; interval_m: number; slope_deg: number; depth_m?: number } | null;
/** 측점마다 소단 = 지금 구조물(초안 따라간 자리)의 소단 구간 · 쪽 깊이는 세션 고침(`bermDepthLeftM` ·
* `bermDepthRightM` · 측점 열쇠) — 노선 초안 동안 소단 고침도 묶음으로(365-5). 구간 밖 측점은 소단 없음. */
export function crossWithBerm(
input: CrossInput | null,
stations: ReadonlyArray<PipelineStation>,
bermAt: BermAt | null,
): CrossInput | null {
if (!input || !bermAt) return input;
const byKey: CrossInput["byKey"] = { ...input.byKey };
for (const station of stations) {
const { bermDepthLeftM, bermDepthRightM, ...rest } = input.byKey[station.key] ?? {};
const at = bermAt(station.chainageM);
byKey[station.key] = {
...rest,
berm: at && {
widthM: at.width_m,
intervalM: at.interval_m,
slopeDeg: at.slope_deg,
depthM: at.depth_m,
depthLeftM: (bermDepthLeftM as number | undefined) ?? null,
depthRightM: (bermDepthRightM as number | undefined) ?? null,
},
};
}
return { ...input, byKey };
}
const kindOf = (station: PipelineStation): SectionStation["kind"] =>
station.key === "end:BP"
? "bp"
@@ -269,7 +238,8 @@ function shapeContext(section: CrossSection): Record<string, unknown> {
};
}
/** 측점마다 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457). */
/** 측점마다 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강 · 소단) = 지금 현황을 새 측점에 다시 얹은 그림에서
* (457) · 소단 쪽 깊이 = 세션 고침(`bermDepthLeftM` · `bermDepthRightM` · 측점 열쇠 · 365-5) · 구간 밖 = 소단 없음. */
export function crossWithShapes(
input: CrossInput | null,
stations: ReadonlyArray<PipelineStation>,
@@ -285,23 +255,31 @@ export function crossWithShapes(
);
const byKey: CrossInput["byKey"] = { ...input.byKey };
stations.forEach((station, i) => {
byKey[station.key] = { ...byKey[station.key], ...shapeContext(sections[i]) };
const { bermDepthLeftM, bermDepthRightM, ...rest } = byKey[station.key] ?? {};
const at = sections[i].berm;
byKey[station.key] = {
...rest,
...shapeContext(sections[i]),
berm: at && {
widthM: at.width_m,
intervalM: at.interval_m,
slopeDeg: at.slope_deg,
depthM: at.depth_m,
depthLeftM: (bermDepthLeftM as number | undefined) ?? null,
depthRightM: (bermDepthRightM as number | undefined) ?? null,
},
};
});
return { ...input, byKey };
}
/** cross 단계 입력 = 시설 맥락(새 자리 그림) · 단면 유형 기본값(상단측) · 소단(지금 구조물) 얹음 — 페이지 묶음 cross. */
/** cross 단계 입력 = 시설 맥락(새 자리 그림 · 소단 포함) · 단면 유형 기본값(상단측) 얹음 — 페이지 묶음 cross. */
export const crossWithContext = (
input: CrossInput | null,
stations: ReadonlyArray<PipelineStation>,
status: ShapeStatus | null,
bermAt: BermAt | null,
): CrossInput | null =>
crossWithBerm(
crossWithUphill(crossWithShapes(crossAtSeat(input, stations), stations, status), stations),
stations,
bermAt,
);
crossWithUphill(crossWithShapes(crossAtSeat(input, stations), stations, status), stations);
/** 묶음 출력 → 상세. 저장 상세의 종단 머리(이름)는 그대로 · 측점 · 지반 · 횡단 · 계획선 표본은 새 노선
* (십자선 대체 높이도 초안 종단 · 413) · 그림 자료 · 카드 이름 = `status`(지금 현황)를 새 측점에 다시 얹음(457). */
+3
View File
@@ -62,6 +62,7 @@ import type { FordPavementSpec } from "./logic/B06_Section_Cross_Ford_Pavement";
import type { RevetmentSpec } from "./logic/B06_Section_Cross_Revetment_Geom";
import type { PavementEntry } from "@util/common_util_structure_pavement";
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
import type { BermSessionSpec } from "@util/common_util_berm_spans";
export type { FordPavementSpec, RevetmentSpec };
export interface SectionContextResponse {
@@ -343,6 +344,8 @@ export interface CrossSection extends SectionStation {
pavement?: PavementEntry[];
/** 맹암거 — 기준 측점(288-1). 높이 · 경사 · 측구 없음 · 노면 기울임 · 돌 무늬 형상. */
underdrain?: UnderdrainSpec;
/** 소단 — 덮는 구간 제원(577-2 · null = 얹었는데 없음 · 칸 없음 = 아직 안 얹음 → 저장분 `design.berm`). */
berm?: BermSessionSpec | null;
/** 빈칸 경고(323-4 · 읽기 칸 · 저장 안 함) — 「종류 칸」(형상 필수 칸이 빈 관 · 구조물 · 측점 계획 자리). */
blank?: string[];
}
+2 -7
View File
@@ -27,8 +27,6 @@
import type { StandardCrossSectionSpec } from "@util/common_util_cross_design";
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
import { readCutSlopeSession, readRockBoundarySession } from "./B06_Section_UI_Page_Persist";
import type { BermSpan } from "./B06_Section_UI_Page_Persist";
import { readState } from "../A00_Common/b_page_state";
import { USER_TOUCHED_KEYS } from "./logic/B06_Section_Design_Keys";
import { savedDesigns } from "./B06_Section_Section_Canon";
import {
@@ -228,17 +226,14 @@ function gatherRefresh(input: CrossRefreshInput): RefreshInputs | null {
const standard = effectiveStandardCross(projectId) as StandardCrossSectionSpec | null;
if (!standard) return null; // 컨텍스트를 아직 못 받음 — 받은 뒤 다시 셈
const { routeId } = input;
// 소단 사본 없음(시설 줄이 구조물을 받기 전) = 저장분 `design.berm`(파이썬 `load_berm_spans` 못 읽음 →
// 저장분 · `stored_berm` 짝 · 467)
const bermCopy = readState<BermSpan[]>("berm", projectId, routeId);
return {
alignment,
standard,
rockOffsets: [...readRockOffsets(projectId, routeId)],
rockDefault: readRockBoundaryDefault(projectId),
// 측점별 암 절토 경사 · 소단 · 카드 선택 · 노견 확장 = 세션에만 있는 값 — **계산 전에** 실음(2026-09-07 · 3-9)
// 측점별 암 절토 경사 · 소단 깊이 · 카드 선택 · 노견 확장 = 세션에만 있는 값 — **계산 전에** 실음(2026-09-07 ·
// 3-9) · 소단 제원 = 측점에 얹힌 `section.berm`(577-2)
cutSlopes: [...readCutSlopeRatios(projectId, routeId)],
bermSpans: Array.isArray(bermCopy) ? bermCopy : null,
bermDepths: [...readBermDepths(projectId, routeId)],
choices: [...crossDesignChoices(projectId, routeId)],
shoulderExtends: [...readShoulderExtends(projectId, routeId)],
+18 -3
View File
@@ -1,7 +1,7 @@
"""형상 얹기 한 벌(323-4) — 측점(횡단 dict) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료.
구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 차례대로 다시 얹음 — 관 세트 → 벽 → 포장 → 맹암거 →
빈칸 경고. 고르기 = 표 형상(322-1). 서버 상세 읽기(`_read_section_detail`) · 재생성 응답이 이것 하나를 씀.
소단 → 빈칸 경고. 고르기 = 표 형상(322-1). 서버 상세 읽기(`_read_section_detail`) · 재생성 응답이 이것 하나를 씀.
셈 = 늘 떠 있는 Node `facility.attach`(화면과 같은 TS · 511-B B-4) ·
파이썬 몫(`_attach_python`)은 Node 를 못 부를 때만.
TS 짝 = `common_util/common_util_structure_attach.ts` `attachShapes`(화면 = 초안 ?? 저장분 현황) ·
@@ -18,7 +18,12 @@ from typing import Any
from B05_Profile.B05_Profile_Structures_Repository import load_structures
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_specs, culvert_sets_from
from B06_Section.B06_Section_Engine_Structures_Wall import attach_wall_specs, wall_specs_from
from B06_Section.B06_Section_Engine_Structures_Wall import (
attach_berm_specs,
attach_wall_specs,
berm_spans_from,
wall_specs_from,
)
from B06_Section.B06_Section_Engine_Tilt import attach_underdrain_specs, underdrains_from
from common_util.common_util_structure_catalog import catalog_row, field_name, type_ids_with
from common_util.common_util_structure_pavement import (
@@ -35,7 +40,16 @@ from common_util.common_util_structure_station import (
logger = logging.getLogger(__name__)
#: 측점 그림 자료 열쇠 — TS `common_util_structure_shape_keys.SHAPE_KEYS` 짝.
SHAPE_KEYS = ("culvert", "ford", "box", "ford_pavement", "revetment", "pavement", "underdrain")
SHAPE_KEYS = (
"culvert",
"ford",
"box",
"ford_pavement",
"revetment",
"pavement",
"underdrain",
"berm",
)
def _blank_labels(type_id: str, options: dict | None, types: dict[str, Any]) -> list[str]:
@@ -128,6 +142,7 @@ def _attach_python(
pavement_specs_from([s.model_dump() for s in structures], type_ids_with(shape="pavement")),
)
attach_underdrain_specs(sections, underdrains_from(structures))
attach_berm_specs(sections, berm_spans_from(structures))
_attach_blanks(sections, pipes, structures)
return misses
@@ -110,11 +110,10 @@ def wall_specs_from(structures: list[Any]) -> list[dict[str, Any]]:
return found
def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None:
"""구조물 정본의 소단 구간 `[(시작, 끝, 제원)]` — [저장] 서버 다시 셈의 소단 원천(194-1).
def berm_spans_from(structures: list[Any]) -> list[tuple[float, float, dict[str, float]]]:
"""구조물 목록 → 소단 구간 `[(시작, 끝, 제원)]`(형상 얹기 한 벌 577-2 · [저장] 194-1).
짝: TS `bermSpansFromStructures`(같은 규칙 — 폭 0 = 없음 · 깊이 빈 값 = 기본 1.0 · 0 미만 = 0).
못 읽으면 None(부르는 쪽이 저장분 `design.berm` 으로 물러섬).
"""
from common_util.common_util_cross_berm import (
BERM_DEFAULT_DEPTH_M,
@@ -130,35 +129,41 @@ def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, fl
return fallback
return value if math.isfinite(value) and value >= 0 else fallback
spans: list[tuple[float, float, dict[str, float]]] = []
for item in structures:
if (catalog_row(item.type_id) or {}).get("shape") != "berm": # 표 형상(323-4)
continue
anchor = item.chainage_m if item.chainage_m is not None else item.start_m
start = item.start_m if item.start_m is not None else anchor
end = item.end_m if item.end_m is not None else anchor
if start is None or end is None:
continue
options = item.options or {}
width = number(options, "width_m", BERM_DEFAULT_WIDTH_M)
if width <= 0:
continue
raw = options.get("depth_m")
try:
depth = BERM_DEFAULT_DEPTH_M if raw in (None, "") else max(float(raw), 0.0)
except (TypeError, ValueError):
depth = BERM_DEFAULT_DEPTH_M
if not math.isfinite(depth):
depth = BERM_DEFAULT_DEPTH_M
spec = {
"width_m": width,
"interval_m": number(options, "interval_m", BERM_DEFAULT_INTERVAL_M),
"slope_deg": number(options, "slope_deg", BERM_DEFAULT_SLOPE_DEG),
"depth_m": depth,
}
spans.append((float(min(start, end)), float(max(start, end)), spec))
return spans
def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None:
"""구조물 정본의 소단 구간 — [저장] 서버 다시 셈의 소단 원천(194-1) · 못 읽으면 None(부르는 쪽이
저장분 `design.berm` 으로 물러섬)."""
try:
spans: list[tuple[float, float, dict[str, float]]] = []
for item in load_structures(str(project_root))[1]:
if (catalog_row(item.type_id) or {}).get("shape") != "berm": # 표 형상(323-4)
continue
anchor = item.chainage_m if item.chainage_m is not None else item.start_m
start = item.start_m if item.start_m is not None else anchor
end = item.end_m if item.end_m is not None else anchor
if start is None or end is None:
continue
options = item.options or {}
width = number(options, "width_m", BERM_DEFAULT_WIDTH_M)
if width <= 0:
continue
raw = options.get("depth_m")
try:
depth = BERM_DEFAULT_DEPTH_M if raw in (None, "") else max(float(raw), 0.0)
except (TypeError, ValueError):
depth = BERM_DEFAULT_DEPTH_M
if not math.isfinite(depth):
depth = BERM_DEFAULT_DEPTH_M
spec = {
"width_m": width,
"interval_m": number(options, "interval_m", BERM_DEFAULT_INTERVAL_M),
"slope_deg": number(options, "slope_deg", BERM_DEFAULT_SLOPE_DEG),
"depth_m": depth,
}
spans.append((float(min(start, end)), float(max(start, end)), spec))
return spans
return berm_spans_from(load_structures(str(project_root))[1])
except Exception: # noqa: BLE001 — 정본을 못 읽으면 저장분으로 물러섬
logger.exception("B06 소단 구간을 읽지 못했습니다 (저장분 소단을 씀)")
return None
@@ -174,6 +179,24 @@ def berm_span_at(
return None
def attach_berm_specs(
cross_sections: list[dict[str, Any]], spans: list[tuple[float, float, dict[str, float]]]
) -> int:
"""측점마다 소단 제원을 얹음(TS `attachBermSpecs` 짝 · 577-2) — 덮는 소단 없음 = None
(얹었음 표시). 얹은 측점 수."""
attached = 0
for section in cross_sections:
chainage = section.get("chainage_m")
if isinstance(chainage, bool) or not isinstance(chainage, (int, float)):
continue
if not math.isfinite(chainage):
continue
spec = berm_span_at(spans, float(chainage))
section["berm"] = dict(spec) if spec else None
attached += 1 if spec else 0
return attached
def attach_wall_specs(cross_sections: list[dict[str, Any]], walls: list[dict[str, Any]]) -> int:
"""구간 안 측점에 벽 제원을 얹는다(TS `attachWallSpecs` 짝). 얹은 개수를 돌려준다."""
if not walls:
+3 -15
View File
@@ -16,7 +16,7 @@ import {
} from "./B06_Section_Api_Fetch";
import type { DesignViewSave } from "../A00_Common/design/design_shared";
import { buildCrossPatches, type CrossPatchSources } from "./logic/B06_Section_UI_Page_Patches";
import { clearState, readByKey, writeByKey, writeState } from "../A00_Common/b_page_state";
import { clearState, readByKey, writeByKey } from "../A00_Common/b_page_state";
import { kindChangedChainages } from "./B06_Section_Api_Culvert_Options";
import {
crossDesignChoices,
@@ -30,15 +30,8 @@ import type { StandardCrossSection } from "./B06_Section_Api_Fetch";
import type { RockBoundaryControl } from "./B06_Section_UI_Section_View";
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
// 소단 구간(C4) — common_util 로 옮김 · 부르는 곳 그대로(다시 내보냄).
import type { BermSpan } from "@util/common_util_berm_spans";
export {
bermSpansFromStructures,
bermSpecAt,
isBermType,
type BermSessionSpec,
type BermSpan,
} from "@util/common_util_berm_spans";
// 소단 종류 판정 — common_util 로 옮김 · 부르는 곳 그대로(다시 내보냄).
export { isBermType } from "@util/common_util_berm_spans";
import { readKeyed, writeKeyed, type KeyedState } from "./B06_Section_Station_Keys";
/**
@@ -124,11 +117,6 @@ function putCell(
}
}
/** 소단 사본(결과 통 · 구조물에서 셈 · 477-4) — 쓰는 곳은 페이지 시설 줄 하나. */
export function writeBermSpans(projectId: string, routeId: number, spans: BermSpan[]): void {
writeState("berm", spans, projectId, routeId);
}
/** 암 경계선 오프셋 저장소 — 값(세션 칸)과 조정창 제어기를 함께 낸다. */
export interface RockBoundaryStore {
/** 지금 세션 칸 — 측점키(누가거리 2자리) → 오프셋(m). `buildCrossPatches` 재료(새 Map). */
@@ -7,7 +7,7 @@
* 순수 — 화면 · fetch · 세션 없음(세션 읽기는 부르는 쪽).
* ========================================================================== */
import { bermSpecAt, type BermSessionSpec, type BermSpan } from "@util/common_util_berm_spans";
import type { BermSessionSpec } from "@util/common_util_berm_spans";
import {
BERM_DEFAULT_DEPTH_M,
BERM_DEFAULT_INTERVAL_M,
@@ -42,8 +42,6 @@ export interface RefreshInputs {
rockDefault: number | null;
/** 측점별 암 절토 경사(0 = 표준값). */
cutSlopes: Array<[number, number]>;
/** 소단 사본(세션 `berm` · 없으면 null = 저장분 `design.berm`). */
bermSpans: BermSpan[] | null;
bermDepths: Array<[number, SideValues]>;
/** 카드 선택(지반 · 측구 · 2단 경사 …). */
choices: Array<[number, CrossDesignChoice]>;
@@ -141,13 +139,12 @@ export function refreshDesigns(
// 측점별 암 절토 경사도 세션에만 있는 값이다 — **계산 전에** 실어야 설계선이 새 경사로
// 그려진다. 계산 뒤에 값만 베껴 붙이면 그림은 옛 경사, 숫자만 새것이 된다(2026-09-07).
const cutSlopeRatios = new Map(inputs.cutSlopes);
// 소단도 같은 성격 — 세션에만 있는 값이라 여기서 실어 주지 않으면 계획선을 고치는
// 순간 계단이 사라진다(계획서 3-9). 사본 없음(시설 줄이 구조물을 받기 전) = 저장분 `design.berm`
// (파이썬 `load_berm_spans` 못 읽음 → 저장분 · `stored_berm` 짝 · 467).
const bermSpans = inputs.bermSpans;
// 소단도 같은 성격 — 계산 전에 실어야 계획선을 고치는 순간 계단이 안 사라진다(계획서 3-9). 제원 = 측점에
// 얹힌 소단(형상 얹기 한 벌 · 577-2) · 아직 안 얹음(칸 없음) = 저장분 `design.berm`(`stored_berm` 짝 · 467)
const bermDepthAt = bermDepthReader(new Map(inputs.bermDepths));
const bermAt = (chainageM: number, design: Record<string, unknown>): BermSpec | null => {
const spec = bermSpans ? bermSpecAt(bermSpans, chainageM) : storedBermSpec(design.berm);
const bermAt = (section: CrossSection, design: Record<string, unknown>): BermSpec | null => {
const chainageM = section.chainage_m;
const spec = section.berm === undefined ? storedBermSpec(design.berm) : section.berm;
return spec
? {
widthM: spec.width_m,
@@ -226,7 +223,7 @@ export function refreshDesigns(
tilted: Boolean(section.ford_pavement || section.underdrain),
tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3)
standard,
berm: bermAt(section.chainage_m, design),
berm: bermAt(section, design),
rockBoundaryOffsetM,
cutSlopeRatio: cutSlopeAt(section.chainage_m, design),
twoStageSlope: choice?.two_stage_slope ?? Boolean(design.two_stage_slope),
+22 -10
View File
@@ -1,10 +1,10 @@
/* =============================================================================
* common_util_berm_spans.ts
* 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4 · 화면 파일
* `B06_Section_UI_Page_Persist.ts` 에서 옮김 · 그쪽이 다시 내보냄).
* 브라우저(시설 줄 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans`)가 같은 셈.
* 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `load_berm_spans` · `berm_span_at`
* (Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py`).
* 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4) · 측점에 얹기(577-2 ·
* 벽처럼 형상 얹기 한 벌 `attachShapes` 가 `section.berm` 을 세움 · 계단은 계산 안쪽 그대로).
* 브라우저(형상 얹기 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans` · `facility.attach`)가 같은 셈.
* 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `berm_spans_from` · `load_berm_spans` · `berm_span_at` ·
* `attach_berm_specs`(Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py` · `test_323_4_attach_mirror.py`).
* ========================================================================== */
import {
@@ -15,7 +15,7 @@ import {
} from "./common_util_cross_berm";
import { catalogRow } from "./common_util_structure_catalog";
/** 측점 하나의 소단 제원 — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
/** 측점 하나의 소단 제원(`section.berm`) — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
export interface BermSessionSpec {
width_m: number;
interval_m: number;
@@ -42,10 +42,6 @@ export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape
/**
* 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다
* (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리).
*
* 세션 열쇠 `berm` 은 그 결과를 담는 **사본**이다. 재계산(브라우저·서버)이 측점마다 값을
* 읽어야 하는데 구조물 목록은 비동기로 오므로, 목록이 바뀔 때마다 여기서 펴 두고 계산은
* 그 사본만 본다.
*/
export function bermSpansFromStructures(
structures: ReadonlyArray<{
@@ -105,3 +101,19 @@ export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpe
}
: null;
}
/** 측점마다 소단 제원을 얹음(파이썬 `attach_berm_specs` 짝 · 제자리) — 덮는 소단 없음 = null(얹었음 표시 ·
* 칸 없음 = 아직 안 얹음 → 다시 셈이 저장분 `design.berm` 으로 물러섬). 얹은 측점 수. */
export function attachBermSpecs(
sections: Array<Record<string, unknown>>,
spans: BermSpan[],
): number {
let attached = 0;
for (const section of sections) {
const chainage = section.chainage_m;
if (typeof chainage !== "number" || !Number.isFinite(chainage)) continue;
section.berm = bermSpecAt(spans, chainage);
if (section.berm) attached += 1;
}
return attached;
}
+6 -1
View File
@@ -3,7 +3,7 @@
* 형상 얹기 한 벌(323-4) — 측점(횡단) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료.
*
* 구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 서버와 같은 차례로 다시 얹음 — 관 세트 → 벽 →
* 포장 → 맹암거 → 빈칸 경고. 고르기 = 표 형상(322-1). 화면은 초안 ?? 저장분 현황으로 이것 하나를 씀
* 포장 → 맹암거 → 소단 → 빈칸 경고. 고르기 = 표 형상(322-1). 화면은 초안 ?? 저장분 현황으로 이것 하나를 씀
* (`B06_Section_Section_Drafts.withDraftShapes`) — 저장 전 그림 = 저장 뒤 그림.
* 파이썬 짝 = `B06_Section/B06_Section_Engine_Attach.py` `attach_shapes`(서버 상세 읽기 · 재생성 응답) ·
* 거울 시험 = `resources/tester/test_323_4_attach_mirror.py`.
@@ -30,6 +30,7 @@ import {
underdrainSpecsFrom,
type UnderdrainInput,
} from "./common_util_structure_underdrain";
import { attachBermSpecs, bermSpansFromStructures } from "./common_util_berm_spans";
import { catalogRow, fieldName } from "./common_util_structure_catalog";
import {
blankKeys,
@@ -163,6 +164,10 @@ export function attachShapes(
),
);
attachUnderdrainSpecs(sections, underdrainSpecsFrom(structures as UnderdrainInput[]));
attachBermSpecs(
sections,
bermSpansFromStructures(structures as Parameters<typeof bermSpansFromStructures>[0]),
);
attachBlanks(sections, pipes, structures, types);
return misses;
}
@@ -13,4 +13,5 @@ export const SHAPE_KEYS = [
"revetment",
"pavement",
"underdrain",
"berm",
] as const;
@@ -35,8 +35,6 @@ const STUBS = {
}),
"ui_template_elements.ts": anyModule(),
"B04_PreProcess_Api_Fetch.ts": anyModule(),
// 소단 사본(467) — 끌어오는 줄이 import.meta 를 써 이 로더로 못 읽음 · 소단 몫은 test_272_berm_source(vite 묶음)
"B06_Section_UI_Page_Persist.ts": anyModule({ bermSpansFromStructures: () => [] }),
};
const resolve = (base) =>
+14 -37
View File
@@ -1,7 +1,7 @@
/* 467 헬퍼 — 소단 원천 한 벌: 구조물 → 소단 구간(TS) · 저장분 소단 제원(TS) · 시설 줄이 세션 사본을 씀 ·
* 다시 셈 = 사본 없으면 저장분 `design.berm`.
/* 467 · 577-2 헬퍼 — 소단 원천 한 벌: 구조물 → 소단 구간(TS) · 측점 얹기(TS) · 저장분 소단 제원(TS) ·
* 다시 셈 = 얹힌 `section.berm` · 칸 없으면 저장분 `design.berm`.
* node helper_272_berm_source.mjs <묶음.mjs> <job.json>
* job = { standard, rockDefault, structureCases, storedCases, ground } · 짝 = `test_272_berm_source.py`.
* job = { standard, rockDefault, structureCases, storedCases, ground, chainages } · 짝 = `test_272_berm_source.py`.
*/
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
@@ -45,34 +45,12 @@ const out = {};
out.spans = job.structureCases.map((list) => m.bermSpansFromStructures(list));
out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec));
/* 2. 시설 줄이 사본을 씀 — 구조물 받기 전 안 씀 · 같으면 안 씀 · 바뀌면 한 번 */
{
m.setPageRouteId(P, R);
const key = m.stateKey("berm", P, R);
const count = () => writes.filter((k) => k === key).length;
const copy = () => m.readState("berm", P, R);
const lane = m.createFacilityLane(P, () => null);
const steps = {};
lane.setTypes([]);
lane.follow(); // 저장분 · 가게 구조물 없음 = 아직 모름
steps.beforeKnown = { copy: copy(), writes: count() };
const saved = job.structureCases[0];
lane.setStored(saved);
lane.set({ structures: saved }, false);
steps.loaded = { copy: copy(), writes: count() };
lane.set({ structures: saved.map((item) => ({ ...item })) });
lane.set({ pipes: [] });
steps.same = { writes: count() };
const moved = saved.map((item) =>
item.type_id === "berm" ? { ...item, start_m: item.start_m + 5, end_m: item.end_m + 5 } : item,
);
lane.set({ structures: moved });
steps.moved = { copy: copy(), writes: count() };
lane.set({ structures: [] });
steps.removed = { copy: copy(), writes: count() };
lane.dispose();
out.lane = steps;
}
/* 2. 측점 얹기 — 구조물마다 측점 목록에 얹은 칸(덮는 소단 없음 = null · 누가거리 없음 = 칸 없음) */
out.attach = job.structureCases.map((list) => {
const sections = job.chainages.map((c) => ({ chainage_m: c }));
m.attachBermSpecs(sections, m.bermSpansFromStructures(list));
return sections;
});
/* 2-2. 시설 줄 칠함 — 종류 표 받기 전 측점(빈 목록)을 종단도에 안 씀 · 받은 뒤 한 번 */
{
@@ -132,10 +110,9 @@ out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec));
],
balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 },
};
const run = async (copy) => {
if (copy === undefined) sessionStorage.removeItem(m.stateKey("berm", P, R));
else m.writeState("berm", copy, P, R);
const run = async (berm) => {
const detail = detailOf();
if (berm !== undefined) detail.cross_sections[0].berm = berm;
await m.refreshCrossDesigns({
projectId: P,
routeId: R,
@@ -147,9 +124,9 @@ out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec));
return { fill: design.fill_area_m2 ?? null, cut: design.cut_area_m2 ?? null };
};
out.refresh = {
noCopy: await run(undefined),
copySame: await run(m.bermSpansFromStructures(job.structureCases[0])),
copyEmpty: await run([]),
unattached: await run(undefined),
attachedSame: await run(job.storedBerm),
attachedNone: await run(null),
};
}
@@ -1,7 +1,7 @@
/* 467 시험 묶음 진입점 — 소단 원천 한 벌(시설 줄 사본 · 다시 셈 저장분 폴백 · 진짜 모듈 · vite ssr). */
/* 467 · 577-2 시험 묶음 진입점 — 소단 원천 한 벌(측점 얹기 · 다시 셈 저장분 폴백 · 진짜 모듈 · vite ssr). */
export { createFacilityLane } from "../../A00_Common/design/design_facility_lane";
export { setPageRouteId } from "../../A00_Common/design/design_page_route";
export { refreshCrossDesigns, storedBermSpec } from "../../B06_Section/B06_Section_Cross_Refresh";
export { bermSpansFromStructures } from "../../B06_Section/B06_Section_UI_Page_Persist";
export { attachBermSpecs, bermSpansFromStructures } from "../../common_util/common_util_berm_spans";
export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel";
export { readState, stateKey, writeState } from "../../A00_Common/b_page_state";
+2 -7
View File
@@ -38,13 +38,6 @@ const R = 6;
const AT = 40;
m.writeState("std-cross", job.standard, P);
m.rememberRockBoundaryDefault(P, job.rockDefault);
// 소단 사본 = 측점을 덮는 구간(소단 깊이가 먹게)
m.writeState(
"berm",
[{ start_m: 0, end_m: 100, width_m: 1, interval_m: 3, slope_deg: 0, depth_m: 1 }],
P,
R,
);
const canonDetail = () => ({
longitudinal: { length_m: 100, stations: [], profile_alignment: { policy: {} } },
cross_sections: [
@@ -59,6 +52,8 @@ const canonDetail = () => ({
center_y: 0,
frame: { left_xy: [0, 1] },
uphill_side: "left",
// 측점에 얹힌 소단(577-2 · 소단 깊이가 먹게)
berm: { width_m: 1, interval_m: 3, slope_deg: 0, depth_m: 1 },
samples: job.ground,
design: {
design_elevation_m: 100,
+20 -10
View File
@@ -80,8 +80,24 @@ STUBS["B05_Profile_Api_Replan"] = {
station_snap: false,
}),
};
// ⑪ 소단 프로젝트 = 저장 구조물에 소단 한 구간(577-2 · 측점에 얹힌 제원으로 셈)
const BERM_SPAN = { from: 30, to: 70 };
STUBS["B05_Profile_Api_Structures"] = {
fetchStructures: async () => ({ structures: [] }),
fetchStructures: async (projectId) => ({
structures:
projectId === "proj-365c"
? [
{
structure_id: "berm-1",
type_id: "berm",
placement: "interval",
start_m: BERM_SPAN.from,
end_m: BERM_SPAN.to,
options: { width_m: 1.5, interval_m: 5, slope_deg: 60, depth_m: 1 },
},
]
: [],
}),
fetchStructureTypes: async () => job.types,
};
STUBS["B05_Profile_Api_TerrainGrid"] = {
@@ -421,17 +437,11 @@ async function run() {
pageMod.disposePagePipeline(Q);
page.clearDrafts(Q);
// ⑪ 초안 소단(365-5) — 소단 = 지금 구조물 구간(bermAt) · 쪽 깊이 = 세션 고침(측점 열쇠) → 묶음 cross
// ⑪ 초안 소단(365-5) — 소단 = 지금 구조물 구간(측점에 얹힌 제원 · 577-2) · 쪽 깊이 = 세션 고침(측점 열쇠) → 묶음 cross
const B = "proj-365c";
window.localStorage.setItem("frd_current_project_id", B);
const span = { from: 30, to: 70 };
const bBridge = pageMod.routeDraftBridge(B, link(), {
stationIntervalM: job.interval,
bermAt: () => (c) =>
c >= span.from && c <= span.to
? { width_m: 1.5, interval_m: 5, slope_deg: 60, depth_m: 1 }
: null,
});
const span = BERM_SPAN;
const bBridge = pageMod.routeDraftBridge(B, link(), { stationIntervalM: job.interval });
pageMod.pipelineEarthwork(B, { conversion: job.conversion });
const bPipe = pageMod.pagePipeline(B);
await settle(runs().length + 1);
@@ -69,10 +69,6 @@ const savedStations = m.buildStations(savedLine(), savedCurves(), pipes, structu
});
const crossSource = m.pipelineCrossSource(P, detail, savedStations, R);
const edits = m.readAlignment(detail.longitudinal).edits;
const bermAt = (list) => {
const spans = m.bermSpansFromStructures(Array.isArray(list) ? list : []);
return (chainageM) => m.bermSpecAt(spans, chainageM);
};
const crossStage = m.createCrossStage({ grid: () => ({ grid }) });
const pipe = m.createDesignPipeline({
stages: {
@@ -119,7 +115,6 @@ const pipe = m.createDesignPipeline({
ctx.inputs.cross,
ctx.out.ground?.stations ?? [],
m.shapeStatus(ctx.inputs.pipes, ctx.inputs.structures, types),
bermAt(ctx.inputs.structures),
),
},
}),
@@ -30,7 +30,6 @@ export {
applyStructureAreaRows,
structureAreaRows,
} from "../../B06_Section/logic/B06_Section_Structure_Layouts";
export { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist";
export { parseDtmGrid } from "../../common_util/common_util_surface_sampler";
export { computeMassHaul } from "../../common_util/common_util_mass_haul";
export { writeState, writeStateRaw } from "../../A00_Common/b_page_state";
@@ -49,7 +49,6 @@ refreshDesigns(
rockOffsets: [],
rockDefault: job.rockDefault,
cutSlopes: [],
bermSpans: null,
bermDepths: [],
choices: [],
shoulderExtends: [],
@@ -183,14 +183,9 @@ await sync.handleDesignChange(20, { two_stage_slope: true }).catch(() => undefin
out.choice = Object.fromEntries(m.crossDesignChoices("pD", 7));
await sync.handleDesignChange(20, {}).catch(() => undefined);
out.choiceAfterEmpty = Object.fromEntries(m.crossDesignChoices("pD", 7));
// 소단 사본(시설 줄이 열 때 구조물에서 씀) — 떠남 확인에 안 잡힘 · 진짜 고침은 잡힘
// 결과 통 칸(계산 묶음 결과) — 떠남 확인에 안 잡힘 · 진짜 고침은 잡힘(소단 사본 칸은 걷음 · 577-2)
sessionStorage.clear();
m.writeState(
"berm",
[{ start_m: 0, end_m: 40, width_m: 1, interval_m: 5, slope_deg: 45, depth_m: 1 }],
"pB",
7,
);
m.writeState("design-stations", [{ chainage_m: 0 }], "pB");
out.dirtyBermOnly = m.designDirty("pB");
m.writeCrossDesignChoice("pB", 7, 20, { ditch_choice: false });
out.dirtyWithEdit = m.designDirty("pB");
@@ -35,8 +35,6 @@ const STUBS = {
}),
"ui_template_elements.ts": anyModule(),
"B04_PreProcess_Api_Fetch.ts": anyModule(),
// 소단 사본(467) — 끌어오는 줄이 import.meta 를 써 이 로더로 못 읽음 · 소단 몫은 test_272_berm_source(vite 묶음)
"B06_Section_UI_Page_Persist.ts": anyModule({ bermSpansFromStructures: () => [] }),
};
const resolve = (base) =>
+50 -26
View File
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
"""467 (요구 272 · 분석 4장 (B2)) — 소단 원천 한 벌.
"""467 · 577-2 (요구 272 · 434 · 437) — 소단 원천 한 벌 = 측점에 얹은 제원(옹벽처럼).
못박는 것
- 소단 사본(세션 `berm`) 쓰는 자리 = 페이지 시설 줄 하나 — 구조물 받기 전 안 씀 ·
같으면 안 씀 · 바뀌면 한 번 · 지우면 빈 목록(B05 만 열어도 씀 · 횡단 보기는 안 씀).
- 다시 셈 = 사본 없으면 저장분 `design.berm`(파이썬 `load_berm_spans` 규칙) —
B05 만 연 값 = B06 연 뒤 값 = [저장] 서버 값(`enforce_design_lines`) · 빈 사본 = 소단 없음.
- 측점 얹기 — 구조물 → 측점마다 `section.berm`(덮는 소단 없음 = null · 겹치면 먼저 놓은 것) ·
TS `attachBermSpecs` ↔ 파이썬 `attach_berm_specs`.
- 다시 셈 = 얹힌 제원 · 칸 없음(아직 안 얹음) = 저장분 `design.berm` · null = 소단 없음 —
얹힌 값 = 저장분 값 = [저장] 서버 값(`enforce_design_lines`).
- 거울 — TS `bermSpansFromStructures` ↔ 파이썬 `load_berm_spans` ·
TS `storedBermSpec` ↔ 파이썬 `berm_from`.
- 세션 사본(`berm` 칸 · 시설 줄 `syncBerm`) 없음.
Node 헬퍼 `helper_272_berm_source.mjs` 가 vite ssr 로 묶은 진짜 모듈을 부름.
"""
@@ -25,7 +26,10 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B06_Section.B06_Section_Engine_Structures_Wall import load_berm_spans # noqa: E402
from B06_Section.B06_Section_Engine_Structures_Wall import ( # noqa: E402
attach_berm_specs,
load_berm_spans,
)
from B06_Section.B06_Section_Router_Design import berm_from, enforce_design_lines # noqa: E402
from config.config_system import ( # noqa: E402
STANDARD_CROSS_SECTION,
@@ -69,6 +73,7 @@ GROUND = [
for o in range(-40, 41)
]
STORED_BERM = {"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0}
CHAINAGES = [0.0, 10.0, 25.0, 30.0, 35.0, 55.0, 65.0, 100.0, 100.5, None]
LONG = {
"design_profiles": [
{
@@ -126,6 +131,7 @@ def out(tmp_path_factory):
"storedCases": STORED_CASES,
"ground": GROUND,
"storedBerm": STORED_BERM,
"chainages": CHAINAGES,
},
ensure_ascii=False,
),
@@ -163,25 +169,27 @@ def test_거울_저장분_소단_제원(out):
assert ts == pytest.approx(want), spec
def test_시설_줄이_사본을_씀(out):
lane = out["lane"]
assert lane["beforeKnown"] == {"copy": None, "writes": 0} # 구조물 받기 전 = 안 씀
assert lane["loaded"]["writes"] == 1
assert lane["loaded"]["copy"] == out["spans"][0] # 저장 구조물 소단 구간
assert lane["same"]["writes"] == 1 # 같은 구간 · 관만 알림 = 안 씀
assert lane["moved"]["writes"] == 2
assert lane["moved"]["copy"][0]["start_m"] == 5.0 and lane["moved"]["copy"][0]["end_m"] == 105.0
assert lane["removed"] == {
"copy": [],
"writes": 3,
} # 소단 지움 = 빈 사본(저장분 소단으로 안 물러섬)
def test_거울_측점_얹기(out, tmp_path):
for index, (structures, ts) in enumerate(zip(STRUCTURE_CASES, out["attach"], strict=True)):
spans = load_berm_spans(_project(tmp_path / f"a{index}", structures))
py = [{"chainage_m": c} for c in CHAINAGES]
attach_berm_specs(py, spans)
assert [("berm" in r) for r in ts] == [("berm" in r) for r in py], index
for got, exp in zip(ts, py, strict=True):
if exp.get("berm") is None:
assert got.get("berm") is None, (index, got, exp)
else:
assert got["berm"] == pytest.approx(exp["berm"]), (index, got, exp)
first = out["attach"][0]
assert first[1]["berm"] == pytest.approx(STORED_BERM) and first[8]["berm"] is None
assert "berm" not in first[-1] # 누가거리 없음 = 칸 없음
def test_다시_셈_B05만_B06_저장_같은_값(out, tmp_path):
r = out["refresh"]
no_copy, same, empty = r["noCopy"], r["copySame"], r["copyEmpty"]
assert no_copy["fill"] == same["fill"] and no_copy["cut"] == same["cut"] # B05 만 = B06 연 뒤
assert empty["fill"] != no_copy["fill"] # 빈 사본 = 소단 없음(저장분으로 안 물러섬)
no_copy, same, empty = r["unattached"], r["attachedSame"], r["attachedNone"]
assert no_copy["fill"] == same["fill"] and no_copy["cut"] == same["cut"] # 안 얹음 = 저장분
assert empty["fill"] != no_copy["fill"] # 얹었는데 없음 = 소단 없음(저장분으로 안 물러섬)
# [저장] = 서버가 구조물 정본으로 다시 셈
root = _project(tmp_path / "save", SAVED)
design = {"ground_type": "soil", "section_mode": "both_fill", "ditch_type": "standard"}
@@ -193,11 +201,27 @@ def test_다시_셈_B05만_B06_저장_같은_값(out, tmp_path):
assert abs(bare[0]["design"]["fill_area_m2"] - empty["fill"]) < 1e-3
def test_쓰는_자리_하나():
sync = (ROOT / "B06_Section" / "B06_Section_UI_Page_Design_Sync.ts").read_text(encoding="utf-8")
assert "writeBermSpans" not in sync and "useStructureIds(projectId, structures)" in sync
lane = (ROOT / "A00_Common" / "design" / "design_facility_lane.ts").read_text(encoding="utf-8")
assert "writeBermSpans(projectId, routeId, next)" in lane
def test_사본_없음():
for rel in (
"A00_Common/design/design_facility_lane.ts",
"A00_Common/b_page_state.ts",
"B06_Section/B06_Section_UI_Page_Persist.ts",
"B06_Section/B06_Section_Cross_Refresh.ts",
"A00_Common/design/design_shared.ts",
):
text = (ROOT / rel).read_text(encoding="utf-8")
for word in (
"writeBermSpans",
"syncBerm",
"putBerm",
'readState<BermSpan[]>("berm"',
"bermAt:",
):
assert word not in text, (rel, word)
shape = (ROOT / "common_util" / "common_util_structure_shape_keys.ts").read_text(
encoding="utf-8"
)
assert '"berm"' in shape
def test_시설_줄_종류_표_전_측점_안_칠함(out):
+5 -1
View File
@@ -188,10 +188,14 @@ def test_every_shape_lands(ts: dict) -> None:
"revetment",
"pavement",
"underdrain",
"berm",
} <= keys
assert any(section.get("berm") for section in ts["cases"]["all"]) # 소단 = 측점에 얹음(577-2)
old = ts["cases"]["all"][1] # 옛 물넘이 자리(5 m) — 걷힘
assert "ford_pavement" not in old
assert all(len(section) == 1 for section in ts["cases"]["empty"])
# 구조물 없음 = 소단 「얹었는데 없음」(null)만
assert all(set(section) == {"chainage_m", "berm"} for section in ts["cases"]["empty"])
assert all(section["berm"] is None for section in ts["cases"]["empty"])
def test_blank_chips(ts: dict) -> None:
+1 -1
View File
@@ -104,6 +104,6 @@ def test_세션_고름은_바꾼_칸끼리_얹음(out):
assert out["choiceAfterEmpty"] == out["choice"]
def test_소단_사본은_떠남_확인에_안_잡힘(out):
def test_결과_통은_떠남_확인에_안_잡힘(out):
assert out["dirtyBermOnly"] is False
assert out["dirtyWithEdit"] is True
+7 -6
View File
@@ -3,7 +3,8 @@
못박는 것
- 477-3 조작 놓은 직후(찍기 기다림 400 ms 안) ↶ 켜짐 · 단추 맞춤 알림 · 바뀐 것 없는 누름은 안 켜짐.
- 기다림 중 ↶ = 먼저 찍고 되돌림(한 번에 조작 전) · ↷ = 다시 · 서버 다시 셈 뒤 쓰기 묶음은 그대로 한 걸음.
- 477-4 `berm` = 결과 통(사진 · 떠남 물음 밖) · 떠남 제외 목록에 사본 줄 없음.
- 477-4 결과 통 칸은 사진 밖 · 소단 사본 칸 `berm` 은 걷음(577-2 · 측점에 얹힌 제원) ·
떠남 목록에 줄 없음.
./venv/Scripts/python.exe -m pytest resources/tester/test_477_undo_now_berm.py -q
"""
@@ -41,7 +42,7 @@ const { createProfileHistory, onPageHistory } = loadTs(process.argv[4]);
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const PIPES = "aislo:draft:pipes:p1";
const CROSS = "aislo:draft:crossdesign:p1:7";
const BERM = "aislo:result:berm:p1:7";
const BERM = "aislo:result:design-stations:p1";
const out = {};
let changes = 0;
onPageHistory({ change: () => (changes += 1) });
@@ -81,7 +82,7 @@ sessionStorage.setItem(PIPES, "b");
lift();
await wait(200);
sessionStorage.setItem(CROSS, "y");
sessionStorage.setItem(BERM, '[{"s":1}]'); // 사본 = 결과 통 — 사진 밖
sessionStorage.setItem(BERM, '[{"s":1}]'); // 결과 통 칸 — 사진 밖
await wait(300);
h2.undo();
out.grouped = [sessionStorage.getItem(PIPES), sessionStorage.getItem(CROSS)];
@@ -128,15 +129,15 @@ def test_기다림_중_되돌리기_먼저_찍고_한_걸음(result) -> None:
def test_서버_다시_셈_묶음_그대로(result) -> None:
assert result["grouped"] == ["a", "x"] and result["groupedLeft"] is False
assert result["bermKept"] == '[{"s":1}]' # 결과 통 사본은 되돌리기가 안 건드림
assert result["bermKept"] == '[{"s":1}]' # 결과 통 칸은 되돌리기가 안 건드림
def test_바뀐_것_없는_누름은_안_켜짐(result) -> None:
assert result["idleCanUndo"] is False
def test_소단_사본은_결과_통() -> None:
def test_소단_사본_칸_없음() -> None:
state = (ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8")
assert re.search(r'^\s*berm: \{ bucket: "result", scope: "route" \}', state, re.M)
assert not re.search(r"^\s*berm: \{", state, re.M) # 577-2 — 측점에 얹힌 제원(사본 걷음)
leave = (ROOT / "A00_Common" / "design" / "design_leave.ts").read_text(encoding="utf-8")
assert '"berm"' not in leave
+3 -3
View File
@@ -57,9 +57,9 @@ def test_자리마다_판_열쇠_저장_길_안_부름():
assert 'calcOnServerNow("quantity")' in refresh and '"F7.saved"' in refresh
shared = _text("A00_Common/design/design_shared.ts")
assert 'calcChecked("quantity", "structure.layouts"' in shared
assert 'calcChecked("quantity", "cross.berm_spans"' in shared
lane = _text("A00_Common/design/design_facility_lane.ts")
assert '"C4.copy", bermRequest(structures)' in lane
# 소단 = 측점에 얹힌 제원(577-2) — 사본 · 초안 구간 셈 부름 없음
assert '"cross.berm_spans"' not in shared
assert '"C4.copy"' not in _text("A00_Common/design/design_facility_lane.ts")
mass = _text("B05_Profile/B05_Profile_UI_Profile_MassHaul.ts")
assert 'calcFromServer<MassHaulSeries[]>("quantity", "masshaul.series"' in mass
assert "openCalcSites(projectId)" in _text("A00_Common/design/design_page.ts")